Community Notes for Servers, now available on FediDB, simply log in with your fediverse account and edit!
fedidb.com
Hashtag
10,737 posts tagged with this hashtag.
Community Notes for Servers, now available on FediDB, simply log in with your fediverse account and edit!
fedidb.com
sigh for reasons unknown to me, I cannot see my Wordpress blog's AP account (@nev@nevillepark.ca) from this account. anyone else, feel free to give it a follow i guess
Community Notes for Servers, now available on FediDB, simply log in with your fediverse account and edit!
fedidb.com
Community Notes for Servers, now available on FediDB, simply log in with your fediverse account and edit!
fedidb.com
Fediverse wiki on FediDB 🔥
fedidb.com
Fediverse wiki on FediDB 🔥
fedidb.com
We will be phasing out communityDB in favour of a more accessible alternative, a fediverse native wiki that you login with your existing fediverse account.
Oof. Until now at least one third of respondents to my #poll feel that it is okay to just refer to #mastodon, when meaning the #fediverse. How is the general public ever going to learn all the fedi and #ActivityPub has to offer, if their judgment is based on this narrow category word that means "microblogging", just one of many fediverse application domains?
social.coop
Has "Mastodon" become a category name, rather than a product / brand name? In a similar way "Jeep" became a category word for "all-terrain vehicle". So often I see people use #Mastodon, where they refer more broadly to #fediverse. And I think "How about all the other fine #microblogging apps other than Mastodon?" Look how many others there are: https://delightful.coding.social/delightful-fediverse-experience/#microblogging What do you think: Has "Mastodon" become a category name? #poll #SocialWeb #ActivityPub [ ] Yes, it has. You can use it instead of fediverse [ ] No, Mastodon is an app, fediverse is category name [ ] Other, please comment
Has "Mastodon" become a category name, rather than a product / brand name? In a similar way "Jeep" became a category word for "all-terrain vehicle".
So often I see people use #Mastodon, where they refer more broadly to #fediverse. And I think "How about all the other fine #microblogging apps other than Mastodon?"
Look how many others there are: https://delightful.coding.social/delightful-fediverse-experience/#microblogging
What do you think: Has "Mastodon" become a category name?
delightful.coding.social
A curated list of fediverse software that offer decentralized social networking services based on the W3C ActivityPub family of related protocols. See also 💎 delightful fediverse development.
We will be phasing out communityDB in favour of a more accessible alternative, a fediverse native wiki that you login with your existing fediverse account.
We will be phasing out communityDB in favour of a more accessible alternative, a fediverse native wiki that you login with your existing fediverse account.
We will be phasing out communityDB in favour of a more accessible alternative, a fediverse native wiki that you login with your existing fediverse account.
The SEO Framework plugin just got even better support for the Fediverse.
"Mastodon can credit a Fediverse profile when someone shares a WordPress URL. TSF outputs the tags that make that credit possible."
https://kb.theseoframework.com/kb/fediverse-creator-and-mastodon/
The SEO Framework plugin just got even better support for the Fediverse.
"Mastodon can credit a Fediverse profile when someone shares a WordPress URL. TSF outputs the tags that make that credit possible."
https://kb.theseoframework.com/kb/fediverse-creator-and-mastodon/
Oof. Until now at least one third of respondents to my #poll feel that it is okay to just refer to #mastodon, when meaning the #fediverse. How is the general public ever going to learn all the fedi and #ActivityPub has to offer, if their judgment is based on this narrow category word that means "microblogging", just one of many fediverse application domains?
social.coop
Has "Mastodon" become a category name, rather than a product / brand name? In a similar way "Jeep" became a category word for "all-terrain vehicle". So often I see people use #Mastodon, where they refer more broadly to #fediverse. And I think "How about all the other fine #microblogging apps other than Mastodon?" Look how many others there are: https://delightful.coding.social/delightful-fediverse-experience/#microblogging What do you think: Has "Mastodon" become a category name? #poll #SocialWeb #ActivityPub [ ] Yes, it has. You can use it instead of fediverse [ ] No, Mastodon is an app, fediverse is category name [ ] Other, please comment
Has "Mastodon" become a category name, rather than a product / brand name? In a similar way "Jeep" became a category word for "all-terrain vehicle".
So often I see people use #Mastodon, where they refer more broadly to #fediverse. And I think "How about all the other fine #microblogging apps other than Mastodon?"
Look how many others there are: https://delightful.coding.social/delightful-fediverse-experience/#microblogging
What do you think: Has "Mastodon" become a category name?
delightful.coding.social
A curated list of fediverse software that offer decentralized social networking services based on the W3C ActivityPub family of related protocols. See also 💎 delightful fediverse development.
Has "Mastodon" become a category name, rather than a product / brand name? In a similar way "Jeep" became a category word for "all-terrain vehicle".
So often I see people use #Mastodon, where they refer more broadly to #fediverse. And I think "How about all the other fine #microblogging apps other than Mastodon?"
Look how many others there are: https://delightful.coding.social/delightful-fediverse-experience/#microblogging
What do you think: Has "Mastodon" become a category name?
delightful.coding.social
A curated list of fediverse software that offer decentralized social networking services based on the W3C ActivityPub family of related protocols. See also 💎 delightful fediverse development.
📸 Vernissage 1.44.0 is out! 🎉
What's new?
- Drag & drop photo uploads
- Workaround for broken uploads on iOS 27
- More cities and improved Spanish translations
- ActivityPub outbox support
- Aggressive SEO and AI crawlers blocked
- Option to disable AI features
- Bug fixes, updated libraries, and new E2E tests
Enjoy the update! ❤️
#Vernissage #ActivityPub #Photography
https://github.com/VernissageApp/VernissageServer/releases/tag/v1.44.0
What's Changed Introduce get outbox for actors by @mczachurski in #611 New cities from geonames service by @mczachurski in #613 Full Changelog: v1.43.0...v1.44.0
📸 Vernissage 1.44.0 is out! 🎉
What's new?
- Drag & drop photo uploads
- Workaround for broken uploads on iOS 27
- More cities and improved Spanish translations
- ActivityPub outbox support
- Aggressive SEO and AI crawlers blocked
- Option to disable AI features
- Bug fixes, updated libraries, and new E2E tests
Enjoy the update! ❤️
#Vernissage #ActivityPub #Photography
https://github.com/VernissageApp/VernissageServer/releases/tag/v1.44.0
What's Changed Introduce get outbox for actors by @mczachurski in #611 New cities from geonames service by @mczachurski in #613 Full Changelog: v1.43.0...v1.44.0
📸 Vernissage 1.44.0 is out! 🎉
What's new?
- Drag & drop photo uploads
- Workaround for broken uploads on iOS 27
- More cities and improved Spanish translations
- ActivityPub outbox support
- Aggressive SEO and AI crawlers blocked
- Option to disable AI features
- Bug fixes, updated libraries, and new E2E tests
Enjoy the update! ❤️
#Vernissage #ActivityPub #Photography
https://github.com/VernissageApp/VernissageServer/releases/tag/v1.44.0
What's Changed Introduce get outbox for actors by @mczachurski in #611 New cities from geonames service by @mczachurski in #613 Full Changelog: v1.43.0...v1.44.0
Raise your hand if #ActivityPub is your favorite standard 👋
#JohnGruber’s return to #XTwitter highlights its density for #techtalk, yet the era of one network hosting all conversation has ended. Smaller #communities on #Bluesky, #Mastodon and #Threads offer trade-offs, but scale no longer guarantees sanity. https://news.lavx.hu/article/is-x-back-who-cares-the-era-of-the-social-media-monolith-is-over?Fedizen.EU #Fedizen #Fediverse #ActivityPub #News
news.lavx.hu
A prominent tech blogger's plan to return to X rehashes an old debate, but the real story is that no single network needs to host the whole conversation anymore. Smaller, focused communities serve people better than one timeline for everyone.
Has "Mastodon" become a category name, rather than a product / brand name? In a similar way "Jeep" became a category word for "all-terrain vehicle".
So often I see people use #Mastodon, where they refer more broadly to #fediverse. And I think "How about all the other fine #microblogging apps other than Mastodon?"
Look how many others there are: https://delightful.coding.social/delightful-fediverse-experience/#microblogging
What do you think: Has "Mastodon" become a category name?
delightful.coding.social
A curated list of fediverse software that offer decentralized social networking services based on the W3C ActivityPub family of related protocols. See also 💎 delightful fediverse development.
Has "Mastodon" become a category name, rather than a product / brand name? In a similar way "Jeep" became a category word for "all-terrain vehicle".
So often I see people use #Mastodon, where they refer more broadly to #fediverse. And I think "How about all the other fine #microblogging apps other than Mastodon?"
Look how many others there are: https://delightful.coding.social/delightful-fediverse-experience/#microblogging
What do you think: Has "Mastodon" become a category name?
delightful.coding.social
A curated list of fediverse software that offer decentralized social networking services based on the W3C ActivityPub family of related protocols. See also 💎 delightful fediverse development.
#JohnGruber’s return to #XTwitter highlights its density for #techtalk, yet the era of one network hosting all conversation has ended. Smaller #communities on #Bluesky, #Mastodon and #Threads offer trade-offs, but scale no longer guarantees sanity. https://news.lavx.hu/article/is-x-back-who-cares-the-era-of-the-social-media-monolith-is-over?Fedizen.EU #Fedizen #Fediverse #ActivityPub #News
news.lavx.hu
A prominent tech blogger's plan to return to X rehashes an old debate, but the real story is that no single network needs to host the whole conversation anymore. Smaller, focused communities serve people better than one timeline for everyone.
This service would be a way to have one identity connected to all the different #p4p communities. Including #ActivityPub (possibly several such services), but also #BlueSky, #Nostr, and many such services.
I'd post votes for which to run. I'd host groups in each, eg: a #SimpleX chat for members.
There are a lot of interesting things to try, and essentially, the service is: making it easy to try them.
Two years ago I asked the #Plone community if @plone had a plan for #ActivityPub and the #Fediverse. Now, things are moving 🙂
community.plone.org
I wonder if there is a plan to support ActivityPub in Plone, or if somebody is working on an add-on for it. WordPress and Drupal already have ActivityPub plugins.
Two years ago I asked the #Plone community if @plone had a plan for #ActivityPub and the #Fediverse. Now, things are moving 🙂
community.plone.org
I wonder if there is a plan to support ActivityPub in Plone, or if somebody is working on an add-on for it. WordPress and Drupal already have ActivityPub plugins.
This service would be a way to have one identity connected to all the different #p4p communities. Including #ActivityPub (possibly several such services), but also #BlueSky, #Nostr, and many such services.
I'd post votes for which to run. I'd host groups in each, eg: a #SimpleX chat for members.
There are a lot of interesting things to try, and essentially, the service is: making it easy to try them.
When Threads was announced and released was the panic huge. Since December 2025 is the Fediverse integration in maintenance mode.
Besides that did almost nobody on Threads use it. It also didn't bring any follows from Threads to @posts.
So what was all that about, and what does it mean for owning your identity online? New post:
https://blog.gelbphoenix.de/threads-and-the-fediverse/
#Blog #Fediverse #Threads #ActivityPub #OpenWeb #DigitalSovereignty
blog.gelbphoenix.de
Why Threads’ Fediverse integration fizzled out, and why owning your domain matters more than any bridge.
When Threads was announced and released was the panic huge. Since December 2025 is the Fediverse integration in maintenance mode.
Besides that did almost nobody on Threads use it. It also didn't bring any follows from Threads to @posts.
So what was all that about, and what does it mean for owning your identity online? New post:
https://blog.gelbphoenix.de/threads-and-the-fediverse/
#Blog #Fediverse #Threads #ActivityPub #OpenWeb #DigitalSovereignty
blog.gelbphoenix.de
Why Threads’ Fediverse integration fizzled out, and why owning your domain matters more than any bridge.
Deadline extended: EU Commission seeks feedback on #EUKidsAct until 3 December 2026:
→ https://ec.europa.eu/info/law/better-regulation/have-your-say/initiatives/19418-Child-online-safety-keeping-digital-spaces-accountable-and-trustworthy-EU-Kids-Act-_en
#HaveYourSay
There are already 2327 comments.
Still not discussed enough: „ #AgeVerification in the #Fediverse: How the KIDS Act challenges #Mastodon “ Can/will #Activitypub , apps and instances adapt?
→ https://www.heise.de/en/background/Age-verification-in-the-Fediverse-How-the-KIDS-Act-challenges-Mastodon-11474306.html
heise.de
The EU’s planned KIDS Act poses technical and organizational challenges for the Fediverse. However, it is unlikely to be the end, as some critics warn.
Deadline extended: EU Commission seeks feedback on #EUKidsAct until 3 December 2026:
→ https://ec.europa.eu/info/law/better-regulation/have-your-say/initiatives/19418-Child-online-safety-keeping-digital-spaces-accountable-and-trustworthy-EU-Kids-Act-_en
#HaveYourSay
There are already 2327 comments.
Still not discussed enough: „ #AgeVerification in the #Fediverse: How the KIDS Act challenges #Mastodon “ Can/will #Activitypub , apps and instances adapt?
→ https://www.heise.de/en/background/Age-verification-in-the-Fediverse-How-the-KIDS-Act-challenges-Mastodon-11474306.html
heise.de
The EU’s planned KIDS Act poses technical and organizational challenges for the Fediverse. However, it is unlikely to be the end, as some critics warn.
Kind of a broad #ActivityPub question, for platforms implementing groups in the #Fediverse
We see maybe 2 or 3 different use-cases for how groups are leveraged in the Fediverse today:
So, I guess my question is: let's say your instance has an index of known remote groups. You just want to show discussion groups, not PeerTube accounts or Flipboard collections. How do you denote the discussion groups, and filter out the rest?
fedizen.net
Fedizen is an experimental Bonfire instance being built specifically to aid network-wide discovery for all things Fediverse-related. Brought to you by the people at We Distribute!
Kind of a broad #ActivityPub question, for platforms implementing groups in the #Fediverse
We see maybe 2 or 3 different use-cases for how groups are leveraged in the Fediverse today:
So, I guess my question is: let's say your instance has an index of known remote groups. You just want to show discussion groups, not PeerTube accounts or Flipboard collections. How do you denote the discussion groups, and filter out the rest?
fedizen.net
Fedizen is an experimental Bonfire instance being built specifically to aid network-wide discovery for all things Fediverse-related. Brought to you by the people at We Distribute!
Sommaire
Walid: hello and welcome to Projets Libres, the LinuxFR podcast that talks about free software, digital commons and open data. I’m Walid Nouh and today, I’m delighted because we’re continuing the series on the Fediverse and we’re going to talk about WordPress and the Fediverse. To talk about it with me, I have someone who is both a blogger and at the same time he’s working on the subject of this WordPress plugin and also the plugin for ATProto, so for BlueSky, we’re going to talk about it right after. So I’m with Jérémy Herve who works at Automattic. That’s it Jeremy, welcome.
Jérémy: Hello, yes I’m Jérémy Herve. I work at Automattic, where I lead a team that is in charge of creating tools for creators, for bloggers, for content creators in general. Part of our job is to maintain and improve the plugin for WordPress that connects any WordPress site, any WordPress blog to the Fediverse.
Walid: I must admit that we already spoke at the WorldCamp in Rennes in September . Unfortunately, my recording didn’t work, so we’re doing it again in good conditions this time. And that I use this plugin myself: you can follow the publications of the WordPress blog. All the information is in the show notes, in the episode description. You have the link to the plugin’s ActivityPub address.
Walid: You said extremely quickly who you are. What I would like is for you to explain to us in a few words how you discovered free software and how did you come to work on WordPress?
Jérémy: I think I’ve always been a fan of free software, I want to say. I’m a blogger, an old blogger now, I want to say. I have my blog that first ran on Joomla and then I switched to WordPress, a little over twenty years ago now. And I had a personal blog for years. which runs and has, like many blogs, I think, seen its activity increase.
I was part of a small community of bloggers, we talked to each other, we commented with each other, we exchanged ideas. Then as the years went by, as social networks grew, we saw this whole conversation move from what was a free world, my WordPress site to a closed interface that was first Facebook and then Twitter, etc.
So it’s true that in my work, I’ve always looked for ways to keep the conversation outside of these prisons, I want to say. And naturally, I had done a little research, I had started, I had created an account on news items, not with Mastodon, but with GoToSocial at the beginning. And then I discovered that indeed, that I could link my passion and my work tool, which was my WordPress blog, with the Fediverse, with a simple extension.
I didn’t need to bother maintaining my own instance on the Fediverse, having to wonder if I had enough RAM on my server to run a Mastodon, to run a GoToSocial, that I could just keep using my blog and participate in conversations.
To have conversations on my blog, but also to have these same conversations on the Fediverse and to have everything open and accessible to everyone. And then I was lucky, I started using this extension myself on my own site. And a few years ago, when Elon Musk took over Twitter and we saw this exodus from Twitter to other platforms, my employer, Automattic, decided to promote and help people who wanted to make this exodus precisely by buying the ActityPub extension and its developer Matthias Pfefferle.
[We proposed] to Matthias, who had been working on this expansion for years already, to continue doing this work not as a simple hobby in his spare time, but really full-time.
So Matthias joined my team, and we started working together. I was able to give him feedback on this expansion, which I had been using for quite some time. And then we worked together to improve it, fix the bugs and make it an even better experience.
Walid: I would put in the description and in the transcript, I would put a link to an interview with Matthias that was given in 2024 on a podcast called WP Build, in which he tells his whole story around Activity Pub, since he started with OStatus , so even before.
He explained in the interview that I listened to again very quickly before we recorded, that the discussions started with Tumblr, because there were questions around Tumblr and the Fediverse, etc. That in fact, the discussions started like that, that in the end, it ended on WordPress and the Fediverse. That’s where there’s really something to do. I had completely forgotten that part. So yes, Matthias, he started working on this extension before: the first commit I saw was in July 2018.
Jérémy: Yes, that’s right. I defined myself as an old blogger at the beginning of the interview. If I’m an old blogger, Matthias is even older than me. He’s an old hand on the web. And indeed, he’s always been part of this small circle of people who worked on OStatus, who worked on Activity Streams, who are the ancestors of the Fediverse we know today.
He worked on a lot of IndieWeb tools in general for everything related to Webmentions, all these tools that allow you to connect sites to each other, I’d like to say. And so, for him, it was natural to expand his work as soon as he could, to start working on a plugin that linked Fediverse and WordPress.
Even today, when we talk about Fediverse and WordPress, the reference person is still Matthias. He also has the advantage, given that he’s been there for years, of knowing people, of being part of working groups on this subject, at the W3C level, at the level of ActivityPub groups as well. And so, it allows him to keep his finger on the pulse of what’s going on in that world, to make suggestions, to help with the development of APIs, and then to bring all these improvements back to the WordPress plugin.
Walid: The extension, it’s evolved a lot. I started using it, I think, maybe at the beginning of 2024, something like that, when I really started to discover the Fediverse. The plugin has evolved a lot since then, but for people who don’t know what this plugin does, can you explain what its features are? What can you do with this plugin, besides a normal WordPress blog?
Jérémy: I want to say that it’s a bit plug and play, in the sense that it’s an extension that you can install, activate and forget. Someone who has an active blog, with comments, will be able to continue writing their blog articles, publishing them, etc. They will always have a comment form under their articles. They will be able to receive comments from people who want to come to the site, leave their email address, their name and publish a comment.
On top of that, people will have the possibility to subscribe to the blog and the author on the blog or to one of the authors on the blog or to all the authors on the blog itself. Today, when you are on a tool like Mastodon for example, you can follow someone else on Mastodon by simply following the Mastodon address which will be @jeremy or something like that.
And so now, we’re going to turn this into the possibility of subscribing to @jeremy@monsite.fr, the address of my WordPress blog. People will be able to follow and receive posts in real time on their timeline on Mastodon. They’ll be able to leave likes, they’ll be able to repost articles and they’ll be able to reply to these posts on Mastodon, without leaving Mastodon, either on their phone or on the Mastodon web interface.
For me, a little blogger, I don’t need to go to Mastodon to see their comments and to interact with them, to thank them, to answer their questions. I’m going to see their answers, their likes, their reposts, I’m going to see them directly in my WordPress interface, as if they were classic comments. And so, we can continue the conversation there, I’m going to answer them from there. And this response, it’s going to be sent back to the Mastodon account, to that person’s Fediverse account.
In the end, it doesn’t change much for the WordPress blogger, in the sense that we stay on his site, we stay on his WordPress dashboard that we know. The only big difference is that we’re going to start seeing interactions and responses from people who, finally, before, were responding to an article on Mastodon and we may not have known that they were talking about us, we may not have known that they were asking us a question, we may not have known that they liked our articles. Now, we can see it, we can answer them, we can thank them.
It brings a little bit of activity back to the blog, which I think needs it. I’ve talked about it, I’m a long-time blogger. And it’s true that I really saw the number of comments on my blog first increase and then gradually decrease until it disappeared completely. I think there was a period around 2019-2020 when I even cut the comments on my blog because, honestly, with the few comments I had, it was either spam or poor quality. It didn’t add anything to my articles.
And having activated the ActivityPub extension, it allowed me to start having comments, real conversations, interesting discussions again. It brought back value to my articles: I went from zero comments on each of my articles to there, lots of likes, lots of reposts, lots of discussions. It brought life back to my little blog. I think it’s really one of the main assets of the extension today.
Walid: Indeed, it’s still quite nice to see discussions from people who interact at least not directly with your blog, but as a behemoth or through the Fediverse tool. And you see comments from people who reply to each other, etc. And it’s really nice to see that, to actually see life on your blog, that the interactions are at home. That’s still something really good. There’s also another part of the plugin that I, personally, don’t use, which is to follow people as well, I think.
Jérémy: Indeed, it’s true that interactions go both ways, it’s true that you can publish articles, receive answers and discuss under these articles. But the Fediverse is not just that. It’s not just about producing writing. It’s also about following other people, liking and reposting their own posts and asking them questions, responding under their own posts too.
And this is indeed something that is possible via the ActivityPub plugin as well. It’s an option that we can activate in the plugin and we end up with a new menu in our WordPress board with a timeline very similar to what we can find on a Mastodon client where we will be able to follow people, respond to them, etc.
So we can find article topics, we can find interesting discussions that we can join, we can respond to those discussions, but the content we write remains under our control because it’s saved from our database, on our WordPress site. And we don’t need to use 36 different tools, 36 different social networks to interact with people. We can do it from a single interface.
Walid: You started with the ActivityPub plugin. BlueSky with its ATProto protocol came along. Finally, you also started to develop a plugin to be able to connect to the network, to the ATmosphere, the whole network around the ATProto protocol.
Eventually, your WordPress becomes your central place where you’ll post messages. People will follow you on the networks they’re on. You don’t have to choose where you want to talk. You can finally talk pretty much anywhere at the same time.
Jérémy: It’s your house that you control, it’s your home, you’re the owner. It’s your content and you don’t need to go looking for readers right and left, you continue to publish at home. Indeed, first of all, we started with ActivityPub because it was first and foremost a project that was open, which allowed this kind of interaction from the start.
BlueSky is basically a micro-blogging tool at the beginning, where you were limited to the number of characters you could publish on an account. The protocol behind Blue Sky, which is called AT Protocol continues to grow: last year, they launched a new part of their protocol called standard.site which no longer allows you to publish small updates to the micro-blogging, but to produce long content that will also be stored and readable on BlueSky.
So we thought it’s the right time to join this network too. So we also developed the ATmosphere extension which works more or less in the same way, I want to say. I publish my article on my WordPress site and this article is sent to people who follow me on BlueSky. They can see all my content, they can respond, they can like, they can repost to them. And all these interactions, they are brought back to my site and the same thing, I can respond to their comments, etc. from my site.
Walid: yes, we typically see on your blog. At the bottom of your article, we see the blog on reactions on social networks. We see both what concerns ActivityPub and BlueSky.

Jérémy: Exactly, that’s right.
Walid: That’s really nice for the moment.
Jérémy: The idea was really that. My home on the web is my blog. And then you can interact with my blog from many different places. For the moment, it’s still the Fediverse and the ATmosphere, but we can hope that in the future, if other open networks develop, why not support them in the same way?
Walid: And so, were there any technical challenges around the development of these plugins? Or around the implementation on a platform like WordPress.com, for example? Did having all these new interactions require infrastructure work? Did it bring any particular challenges?
Jérémy: It requires reflection, for sure. On the one hand, because it’s true that at the beginning, WordPress is first and foremost a blog engine, even today. It’s true that today, it’s also a content manager, you can do a lot of things with it. But you have to think about the infrastructure, how you’re going to organize this data on the blog. Should a message that is published by someone I follow be stored like an article? Like a comment? Something else, etc? So you have to think a little bit about how you organize the data.
But I think that honestly, the most interesting, the most difficult challenge, on which we still suffer a little bit, or at least we don’t suffer, but it’s something you have to be aware of when today you activate the ActivityPub extension on your site, is that since people can follow you from anywhere, when you publish an article today, this article, it can be sent to hundreds of people, dozens of people, it depends on how many followers you have, who are going to be on many different instances of the Fediverse.
So someone will follow you from mastodon.social, someone will follow you from another Mastodon instance in Germany, the United States, etc., right and left. As soon as you press the publish button, all these instances will receive a notification that there is a new article, and they will in return come to visit your site to see « ok, what is this new article? Let me take a representative image, a description, a small summary, etc ».
And we end up with an effect that we already had in the 2000s with Digg for example, or with Slashdot, where our site is bombarded with requests from many different places that want to access our data. And so, if we’re not careful, if we don’t have a suitable cache plugin on the site, you can end up with your site that’s down for a few minutes because it’s hammered with requests.
Walid: Are there any particular challenges around the integration with ATProto?
Jérémy: ATProto is still something that is young, I want to say. And so, we are building it while keeping an eye on what is happening with the protocol today, because it changes every month, there are new tools that are created, etc.
As the protocol becomes more and more decentralized, because it’s true that today, when we talk about ATProto, we’re talking about BlueSky, essentially. But the goal is for the AT Protocol to be more than just BlueSky.
For us, in France and in Europe, we have Eurosky now, the latest one. We also have W.social, which was launched a few months ago as well, which we talk about a lot too. So, our goal, on our side, is really to keep an eye on all this and to make sure that our tool, already, continues to work well, that it works with the whole protocol, not just with BlueSky.
And when we talk about our extension, we’re not just talking about BlueSky, because we also want to promote things like Eurosky and open alternatives. So, that’s the biggest thing to pay attention to today, I want to say: stay on top of what’s going on on the protocol and stay open to everyone.
Walid: OK. Super interesting. I think we’ve done the trick for that part. I’m really happy, because I contacted you a long time ago to talk about Activity Pub and WordPress and then I didn’t have time to prepare everything so now I’m really happy that we were able to record this, I wanted to leave you a last word, do you have a message to pass on to the listeners of Projets Libres before we leave?
Jérémy: I want to say that if you have a WordPress blog or if you wanted to join Mastodon and you haven’t taken the plunge yet, well now is the time to try. Why not? And if you have anything to complain about, if you have any feedback on the experience with the plugin, if you have any questions, Matthias and I, we can join us on the Fediverse, and also on the support forums of the extension.
So we’re there, we answer questions, we try to answer as quickly as possible. And the plugin, we work on it every day, every week, we make a lot of changes, and so if people have feedback and good ideas, we’re happy to integrate them into the extension, so don’t hesitate.
Walid: Well, Jérémy, thank you very much. It was a pleasure to have you talk about this subject that is particularly close to my heart, which is the Fediverse. For the listeners, if you have any questions, do not hesitate to answer the news item, to ask your questions, to give your remarks. It will be with great pleasure. Jérémy, thank you very much.
Jérémy: thank you.
Walid: See you soon. And then, I’ll be happy to follow the evolution of the plugin that I use myself too.
Jeremy: Thank you.
Walid: Thank you, goodbye.
You can consult our AI charter.
Production:
Publication:
This podcast is released under the CC BY-SA 4.0 license or later
Sommaire
Walid: hello and welcome to Projets Libres, the LinuxFR podcast that talks about free software, digital commons and open data. I’m Walid Nouh and today, I’m delighted because we’re continuing the series on the Fediverse and we’re going to talk about WordPress and the Fediverse. To talk about it with me, I have someone who is both a blogger and at the same time he’s working on the subject of this WordPress plugin and also the plugin for ATProto, so for BlueSky, we’re going to talk about it right after. So I’m with Jérémy Herve who works at Automattic. That’s it Jeremy, welcome.
Jérémy: Hello, yes I’m Jérémy Herve. I work at Automattic, where I lead a team that is in charge of creating tools for creators, for bloggers, for content creators in general. Part of our job is to maintain and improve the plugin for WordPress that connects any WordPress site, any WordPress blog to the Fediverse.
Walid: I must admit that we already spoke at the WorldCamp in Rennes in September . Unfortunately, my recording didn’t work, so we’re doing it again in good conditions this time. And that I use this plugin myself: you can follow the publications of the WordPress blog. All the information is in the show notes, in the episode description. You have the link to the plugin’s ActivityPub address.
Walid: You said extremely quickly who you are. What I would like is for you to explain to us in a few words how you discovered free software and how did you come to work on WordPress?
Jérémy: I think I’ve always been a fan of free software, I want to say. I’m a blogger, an old blogger now, I want to say. I have my blog that first ran on Joomla and then I switched to WordPress, a little over twenty years ago now. And I had a personal blog for years. which runs and has, like many blogs, I think, seen its activity increase.
I was part of a small community of bloggers, we talked to each other, we commented with each other, we exchanged ideas. Then as the years went by, as social networks grew, we saw this whole conversation move from what was a free world, my WordPress site to a closed interface that was first Facebook and then Twitter, etc.
So it’s true that in my work, I’ve always looked for ways to keep the conversation outside of these prisons, I want to say. And naturally, I had done a little research, I had started, I had created an account on news items, not with Mastodon, but with GoToSocial at the beginning. And then I discovered that indeed, that I could link my passion and my work tool, which was my WordPress blog, with the Fediverse, with a simple extension.
I didn’t need to bother maintaining my own instance on the Fediverse, having to wonder if I had enough RAM on my server to run a Mastodon, to run a GoToSocial, that I could just keep using my blog and participate in conversations.
To have conversations on my blog, but also to have these same conversations on the Fediverse and to have everything open and accessible to everyone. And then I was lucky, I started using this extension myself on my own site. And a few years ago, when Elon Musk took over Twitter and we saw this exodus from Twitter to other platforms, my employer, Automattic, decided to promote and help people who wanted to make this exodus precisely by buying the ActityPub extension and its developer Matthias Pfefferle.
[We proposed] to Matthias, who had been working on this expansion for years already, to continue doing this work not as a simple hobby in his spare time, but really full-time.
So Matthias joined my team, and we started working together. I was able to give him feedback on this expansion, which I had been using for quite some time. And then we worked together to improve it, fix the bugs and make it an even better experience.
Walid: I would put in the description and in the transcript, I would put a link to an interview with Matthias that was given in 2024 on a podcast called WP Build, in which he tells his whole story around Activity Pub, since he started with OStatus , so even before.
He explained in the interview that I listened to again very quickly before we recorded, that the discussions started with Tumblr, because there were questions around Tumblr and the Fediverse, etc. That in fact, the discussions started like that, that in the end, it ended on WordPress and the Fediverse. That’s where there’s really something to do. I had completely forgotten that part. So yes, Matthias, he started working on this extension before: the first commit I saw was in July 2018.
Jérémy: Yes, that’s right. I defined myself as an old blogger at the beginning of the interview. If I’m an old blogger, Matthias is even older than me. He’s an old hand on the web. And indeed, he’s always been part of this small circle of people who worked on OStatus, who worked on Activity Streams, who are the ancestors of the Fediverse we know today.
He worked on a lot of IndieWeb tools in general for everything related to Webmentions, all these tools that allow you to connect sites to each other, I’d like to say. And so, for him, it was natural to expand his work as soon as he could, to start working on a plugin that linked Fediverse and WordPress.
Even today, when we talk about Fediverse and WordPress, the reference person is still Matthias. He also has the advantage, given that he’s been there for years, of knowing people, of being part of working groups on this subject, at the W3C level, at the level of ActivityPub groups as well. And so, it allows him to keep his finger on the pulse of what’s going on in that world, to make suggestions, to help with the development of APIs, and then to bring all these improvements back to the WordPress plugin.
Walid: The extension, it’s evolved a lot. I started using it, I think, maybe at the beginning of 2024, something like that, when I really started to discover the Fediverse. The plugin has evolved a lot since then, but for people who don’t know what this plugin does, can you explain what its features are? What can you do with this plugin, besides a normal WordPress blog?
Jérémy: I want to say that it’s a bit plug and play, in the sense that it’s an extension that you can install, activate and forget. Someone who has an active blog, with comments, will be able to continue writing their blog articles, publishing them, etc. They will always have a comment form under their articles. They will be able to receive comments from people who want to come to the site, leave their email address, their name and publish a comment.
On top of that, people will have the possibility to subscribe to the blog and the author on the blog or to one of the authors on the blog or to all the authors on the blog itself. Today, when you are on a tool like Mastodon for example, you can follow someone else on Mastodon by simply following the Mastodon address which will be @jeremy or something like that.
And so now, we’re going to turn this into the possibility of subscribing to @jeremy@monsite.fr, the address of my WordPress blog. People will be able to follow and receive posts in real time on their timeline on Mastodon. They’ll be able to leave likes, they’ll be able to repost articles and they’ll be able to reply to these posts on Mastodon, without leaving Mastodon, either on their phone or on the Mastodon web interface.
For me, a little blogger, I don’t need to go to Mastodon to see their comments and to interact with them, to thank them, to answer their questions. I’m going to see their answers, their likes, their reposts, I’m going to see them directly in my WordPress interface, as if they were classic comments. And so, we can continue the conversation there, I’m going to answer them from there. And this response, it’s going to be sent back to the Mastodon account, to that person’s Fediverse account.
In the end, it doesn’t change much for the WordPress blogger, in the sense that we stay on his site, we stay on his WordPress dashboard that we know. The only big difference is that we’re going to start seeing interactions and responses from people who, finally, before, were responding to an article on Mastodon and we may not have known that they were talking about us, we may not have known that they were asking us a question, we may not have known that they liked our articles. Now, we can see it, we can answer them, we can thank them.
It brings a little bit of activity back to the blog, which I think needs it. I’ve talked about it, I’m a long-time blogger. And it’s true that I really saw the number of comments on my blog first increase and then gradually decrease until it disappeared completely. I think there was a period around 2019-2020 when I even cut the comments on my blog because, honestly, with the few comments I had, it was either spam or poor quality. It didn’t add anything to my articles.
And having activated the ActivityPub extension, it allowed me to start having comments, real conversations, interesting discussions again. It brought back value to my articles: I went from zero comments on each of my articles to there, lots of likes, lots of reposts, lots of discussions. It brought life back to my little blog. I think it’s really one of the main assets of the extension today.
Walid: Indeed, it’s still quite nice to see discussions from people who interact at least not directly with your blog, but as a behemoth or through the Fediverse tool. And you see comments from people who reply to each other, etc. And it’s really nice to see that, to actually see life on your blog, that the interactions are at home. That’s still something really good. There’s also another part of the plugin that I, personally, don’t use, which is to follow people as well, I think.
Jérémy: Indeed, it’s true that interactions go both ways, it’s true that you can publish articles, receive answers and discuss under these articles. But the Fediverse is not just that. It’s not just about producing writing. It’s also about following other people, liking and reposting their own posts and asking them questions, responding under their own posts too.
And this is indeed something that is possible via the ActivityPub plugin as well. It’s an option that we can activate in the plugin and we end up with a new menu in our WordPress board with a timeline very similar to what we can find on a Mastodon client where we will be able to follow people, respond to them, etc.
So we can find article topics, we can find interesting discussions that we can join, we can respond to those discussions, but the content we write remains under our control because it’s saved from our database, on our WordPress site. And we don’t need to use 36 different tools, 36 different social networks to interact with people. We can do it from a single interface.
Walid: You started with the ActivityPub plugin. BlueSky with its ATProto protocol came along. Finally, you also started to develop a plugin to be able to connect to the network, to the ATmosphere, the whole network around the ATProto protocol.
Eventually, your WordPress becomes your central place where you’ll post messages. People will follow you on the networks they’re on. You don’t have to choose where you want to talk. You can finally talk pretty much anywhere at the same time.
Jérémy: It’s your house that you control, it’s your home, you’re the owner. It’s your content and you don’t need to go looking for readers right and left, you continue to publish at home. Indeed, first of all, we started with ActivityPub because it was first and foremost a project that was open, which allowed this kind of interaction from the start.
BlueSky is basically a micro-blogging tool at the beginning, where you were limited to the number of characters you could publish on an account. The protocol behind Blue Sky, which is called AT Protocol continues to grow: last year, they launched a new part of their protocol called standard.site which no longer allows you to publish small updates to the micro-blogging, but to produce long content that will also be stored and readable on BlueSky.
So we thought it’s the right time to join this network too. So we also developed the ATmosphere extension which works more or less in the same way, I want to say. I publish my article on my WordPress site and this article is sent to people who follow me on BlueSky. They can see all my content, they can respond, they can like, they can repost to them. And all these interactions, they are brought back to my site and the same thing, I can respond to their comments, etc. from my site.
Walid: yes, we typically see on your blog. At the bottom of your article, we see the blog on reactions on social networks. We see both what concerns ActivityPub and BlueSky.

Jérémy: Exactly, that’s right.
Walid: That’s really nice for the moment.
Jérémy: The idea was really that. My home on the web is my blog. And then you can interact with my blog from many different places. For the moment, it’s still the Fediverse and the ATmosphere, but we can hope that in the future, if other open networks develop, why not support them in the same way?
Walid: And so, were there any technical challenges around the development of these plugins? Or around the implementation on a platform like WordPress.com, for example? Did having all these new interactions require infrastructure work? Did it bring any particular challenges?
Jérémy: It requires reflection, for sure. On the one hand, because it’s true that at the beginning, WordPress is first and foremost a blog engine, even today. It’s true that today, it’s also a content manager, you can do a lot of things with it. But you have to think about the infrastructure, how you’re going to organize this data on the blog. Should a message that is published by someone I follow be stored like an article? Like a comment? Something else, etc? So you have to think a little bit about how you organize the data.
But I think that honestly, the most interesting, the most difficult challenge, on which we still suffer a little bit, or at least we don’t suffer, but it’s something you have to be aware of when today you activate the ActivityPub extension on your site, is that since people can follow you from anywhere, when you publish an article today, this article, it can be sent to hundreds of people, dozens of people, it depends on how many followers you have, who are going to be on many different instances of the Fediverse.
So someone will follow you from mastodon.social, someone will follow you from another Mastodon instance in Germany, the United States, etc., right and left. As soon as you press the publish button, all these instances will receive a notification that there is a new article, and they will in return come to visit your site to see « ok, what is this new article? Let me take a representative image, a description, a small summary, etc ».
And we end up with an effect that we already had in the 2000s with Digg for example, or with Slashdot, where our site is bombarded with requests from many different places that want to access our data. And so, if we’re not careful, if we don’t have a suitable cache plugin on the site, you can end up with your site that’s down for a few minutes because it’s hammered with requests.
Walid: Are there any particular challenges around the integration with ATProto?
Jérémy: ATProto is still something that is young, I want to say. And so, we are building it while keeping an eye on what is happening with the protocol today, because it changes every month, there are new tools that are created, etc.
As the protocol becomes more and more decentralized, because it’s true that today, when we talk about ATProto, we’re talking about BlueSky, essentially. But the goal is for the AT Protocol to be more than just BlueSky.
For us, in France and in Europe, we have Eurosky now, the latest one. We also have W.social, which was launched a few months ago as well, which we talk about a lot too. So, our goal, on our side, is really to keep an eye on all this and to make sure that our tool, already, continues to work well, that it works with the whole protocol, not just with BlueSky.
And when we talk about our extension, we’re not just talking about BlueSky, because we also want to promote things like Eurosky and open alternatives. So, that’s the biggest thing to pay attention to today, I want to say: stay on top of what’s going on on the protocol and stay open to everyone.
Walid: OK. Super interesting. I think we’ve done the trick for that part. I’m really happy, because I contacted you a long time ago to talk about Activity Pub and WordPress and then I didn’t have time to prepare everything so now I’m really happy that we were able to record this, I wanted to leave you a last word, do you have a message to pass on to the listeners of Projets Libres before we leave?
Jérémy: I want to say that if you have a WordPress blog or if you wanted to join Mastodon and you haven’t taken the plunge yet, well now is the time to try. Why not? And if you have anything to complain about, if you have any feedback on the experience with the plugin, if you have any questions, Matthias and I, we can join us on the Fediverse, and also on the support forums of the extension.
So we’re there, we answer questions, we try to answer as quickly as possible. And the plugin, we work on it every day, every week, we make a lot of changes, and so if people have feedback and good ideas, we’re happy to integrate them into the extension, so don’t hesitate.
Walid: Well, Jérémy, thank you very much. It was a pleasure to have you talk about this subject that is particularly close to my heart, which is the Fediverse. For the listeners, if you have any questions, do not hesitate to answer the news item, to ask your questions, to give your remarks. It will be with great pleasure. Jérémy, thank you very much.
Jérémy: thank you.
Walid: See you soon. And then, I’ll be happy to follow the evolution of the plugin that I use myself too.
Jeremy: Thank you.
Walid: Thank you, goodbye.
You can consult our AI charter.
Production:
Publication:
This podcast is released under the CC BY-SA 4.0 license or later
Fedify 2.4.0 is out.
The main addition is support for FEP-ef61 portable objects. A Fedify server can now host actors whose identity is a DID instead of a domain, serve their signed objects through /.well-known/apgateway/, and verify the portable objects of other servers. I tested it against tootik and Mitra, and wrote up where Fedify's profile differs from the FEP and what it leaves out.
The release also adds application-defined background tasks, the ActivityPub Media Upload extension, per-request inbox reports, 410 Gone for deleted objects, and four new packages (AdonisJS, Netlify, PGlite, and interaction controls).
Some defaults changed, so please read the upgrading section before you update, especially if you federate with software that uses FEP-ef61 compatible identifiers.
Release notes: https://hackers.pub/@fedify/2026/fedify-2-4.
Thanks to everyone who contributed to this cycle, and to @silverpill for patient answers about the FEP.
hackers.pub
Fedify is a TypeScript framework for building ActivityPub servers. It implements federation details such as HTTP Signatures, JSON-LD processing, WebFinger, inbox and outbox routing, and activity delivery.
Fedify 2.4.0 is a minor release with an unusually long changelog. The main addition is support for FEP-ef61 portable objects: a Fedify server can now host actors whose identity is a DID rather than a domain, serve and verify their signed objects, and consume the portable objects of other servers. The release also adds an API for application-defined background tasks, the ActivityPub Media Upload extension, a hook that reports how every inbox request was authenticated and handled, and 410 Gone tombstones for deleted objects. Several defaults change to limit what a hostile peer can make Fedify do. Four new packages ship with this version: @fedify/adonisjs, @fedify/interaction-controls, @fedify/netlify, and @fedify/pglite.
Most applications can upgrade without code changes, but a few of the new defaults are behavior changes. They are listed in the Upgrading section at the end.
An ActivityPub object's ID is an HTTP(S) URL on its server's domain. If that server shuts down, its IDs stop resolving, and an actor that moves elsewhere gets a new ID that everyone has to follow again. FEP-ef61, written by @silverpill of Mitra, proposes IDs that do not name a server at all. A portable ID is an ap: URI whose authority is a DID:
ap://did:key:z6MkrJVnaZkeFzdQyMZu1cgjg7k1pZZ6pvBQ7XJPt4swbTQ2/actor
The same object can be stored on several servers, called gateways, each of which serves it under /.well-known/apgateway/. Because no server owns the ID, a portable actor, activity, or object is authenticated only by its FEP-8b32 Object Integrity Proof, made with a key of the DID in its ID. A portable actor lists its gateways in a gateways property, and other servers deliver to its inbox through them.
Fedify 2.4.0 implements a profile of FEP-ef61 for gateways and consumers of portable objects. It makes two deliberate choices where the FEP or the specifications it rests on leave questions open, and it leaves out a few parts of the proposal; both are described below. The new Portable objects chapter of the manual walks through running portable actors from start to finish and then describes the profile as a whole, so that other implementers can tell what to expect from a Fedify server. The request came from @anotherdoesnm in #288, which tracked the work from a roadmap to the final pieces.
Portable objects are a foundation for nomadic identity, not nomadic identity itself. Fedify serves and verifies portable objects and forwards activities between gateways, but it does not copy actors or posts between servers, rotate keys, or move an actor to another DID. Full nomadic identity is still open in #413.
A portable actor starts with an Ed25519 key pair. Its DID is made from the public key with exportDidKey(), and whoever holds the private key can act as the actor, so store it with the actor and keep it safe:
import { generateCryptoKeyPair } from "@fedify/fedify";
import { exportDidKey } from "@fedify/vocab-runtime";
const { publicKey, privateKey } = await generateCryptoKeyPair("Ed25519");
const did = await exportDidKey(publicKey); // did:key:z6Mk...
// The verification method that the actor's proofs refer to:
const didKeyId = new URL(`${did}#${did.slice("did:key:".length)}`);
The actor dispatcher returns a portable actor as it would an ordinary one. Context.getPortableActorUri() and its siblings for inboxes, outboxes, collections, and objects build portable IDs from the dispatcher's paths and the DID, and signObject() signs the actor with the DID's key:
federation
.setActorDispatcher("/users/{identifier}", async (ctx, identifier) => {
const user = await findUser(identifier);
if (user == null) return null;
// A request through the gateway endpoint names the DID in its path,
// e.g., GET /.well-known/apgateway/did:key:z6Mk.../users/alice:
if (
ctx.portableRequest != null &&
ctx.portableRequest.authority !== user.did
) {
return null;
}
// This server's gateway keys for the actor (see below):
const keys = await ctx.getActorKeyPairs(identifier);
return await signObject(
new Person({
id: ctx.getPortableActorUri(identifier, user.did), // ap+ef61://did:key:z6Mk.../users/alice
preferredUsername: user.username,
inbox: ctx.getPortableInboxUri(identifier, user.did),
outbox: ctx.getPortableOutboxUri(identifier, user.did),
followers: ctx.getPortableFollowersUri(identifier, user.did),
gateways: [new URL("https://example.com")],
publicKeys: keys.map((key) => key.cryptographicKey),
assertionMethods: keys.map((key) => key.multikey),
}),
user.didKeyPair.privateKey,
new URL(`${user.did}#${user.did.slice("did:key:".length)}`),
);
})
.setKeyPairsDispatcher(async (ctx, identifier) => {
const user = await findUser(identifier);
// This server's own key pair for the actor, not the DID's:
return user == null ? [] : [user.gatewayKeyPair];
})
.mapPortableActorId(async (ctx, identifier) => {
const user = await findUser(identifier);
return user == null ? null : ctx.getPortableActorUri(identifier, user.did);
});
With this dispatcher, Fedify serves the actor at its compatible identifier, https://example.com/.well-known/apgateway/did:key:z6Mk.../users/alice, which is how other servers retrieve it. It serves it through WebFinger as @alice@example.com, since the handle's domain comes from the first gateway in gateways, and at its ordinary URL as before. On the gateway endpoint, Fedify serves a document only if its ID is the requested portable ID and it carries a proof made with a key of that DID.
The key pairs dispatcher and the new ActorCallbackSetters.mapPortableActorId() give the actor gateway keys. These are the server's own keys for the actor, and they sign only the HTTP requests the server makes on the actor's behalf, such as deliveries. They never sign the actor's activities or objects; only the DID's key does that. The actor document lists them under assertionMethods, as FEP-521a describes, so that other servers can verify the HTTP Signatures. See Dispatching the actor and Gateway keys of portable actors for the details.
Object and collection dispatchers serve portable objects and collections through the gateway endpoint in the same way, routed by the path after the DID. Two rules protect the actor's data. A non-public portable object is served only if the dispatcher has an authorization predicate, because FEP-ef61 forbids a gateway to show it to anyone outside its audience; the new RequestContext.isSignedByAudience() helps to write one. Without a predicate, a request for such an object gets 404 Not Found, as though the gateway did not store it. A deleted object is served as a signed Tombstone with 410 Gone. Resources that portable objects refer to by SHA-256 hashlinks, such as GET /.well-known/apgateway/hl:zQm..., are served by the new Federatable.setHashlinkMediaDispatcher().
A portable actor's activities have to be signed by its DID, which Fedify's key pairs dispatcher does not know about. Sign them with signObject() before sending, and pass normalizeExistingProofs: true so that the activity goes out in the form its proof covers. An activity that embeds a portable object, such as the Note of a Create, is signed inside out:
const actor = withGatewayHints(
ctx.getPortableActorUri(user.identifier, user.did),
[new URL("https://example.com")],
); // ap+ef61://did:key:z6Mk.../users/alice?@gateway=https%3A%2F%2Fexample.com
const note = await signObject(
new Note({
id: ctx.getPortableObjectUri(
Note,
{ identifier: user.identifier, id: crypto.randomUUID() },
user.did,
),
attribution: actor,
to: PUBLIC_COLLECTION,
content: "Hello, world!",
}),
user.didKeyPair.privateKey,
didKeyId,
);
const create = await signObject(
new Create({
id: ctx.getPortableObjectUri(
Note,
{ identifier: user.identifier, id: crypto.randomUUID() },
user.did,
),
actor,
to: PUBLIC_COLLECTION,
object: note,
}),
user.didKeyPair.privateKey,
didKeyId,
);
await ctx.sendActivity(
{ identifier: user.identifier },
"followers",
create,
{ normalizeExistingProofs: true },
);
This example depends on a fix to signObject() that is useful outside FEP-ef61 as well. Previously, a signed object assigned to a typed parent was rebuilt under the parent's JSON-LD context when the parent was serialized. The child kept its proofValue but lost the document its proof covered, so it no longer verified on its own. signObject() now captures the exact document it signed, and nested serialization embeds that document verbatim. The captured document is a snapshot: clone() does not carry it, so sign a clone again if it has to be embedded. The design is in #1044 and the fix in #1051.
Context.sendActivity() refuses a portable activity that is unsigned, signed by the wrong key, or carries a Linked Data Signature, and throws a TypeError before anything is queued. It also refuses a few shapes of compound document that Fedify's own inboxes would reject, such as a received signed Follow embedded in an Accept; refer to such an object by its ID instead. Deliveries to portable inboxes go through the recipient's gateways, or else the @gateway hints on the inbox, trying at most five of them until one accepts. Before this release, they failed with UrlError: Unsupported protocol: ap+ef61:. The rules are in Delivering to portable actors and Producing a compound document.
On the receiving side, a portable inbox is reached as POST /.well-known/apgateway/did:key:z6Mk.../users/alice/inbox and dispatched to the same inbox listeners as the ordinary inbox. Context.parseUri() gains a portable option, and the parsed result has the DID in a new authority property to check against the one you store. Fedify accepts an activity of a portable actor, or one with a portable ID, only if it has a valid proof by that DID; neither an HTTP Signature nor a Linked Data Signature authenticates it. Each portable object embedded in the activity is verified against its own proof before dispatch. An accepted activity that carries its own proof is forwarded to the actor's other gateways at most once, as FEP-ef61 recommends. Forwarding needs a key–value store that supports cas(), and the FederationOptions.portableInboxForwarding option tunes it. See Portable inboxes and Forwarding to other gateways.
Applications that never host a portable actor still meet portable objects from other servers. Context.lookupObject() resolves portable IDs, compatible identifiers, and the fediverse handles of portable actors, and returns portable objects only if they verify:
const actor = await ctx.lookupObject("@alice@example.com");
// A portable ID without @gateway hints needs gateways to ask:
const note = await ctx.lookupObject("ap://did:key:z6Mk.../notes/1", {
gateways: ["https://example.com"],
});
Property accessors fetch portable references through gateways as well, and they verify what they fetch with the new verifyPortableObject option. Context has a matching verifyPortableObject property, so passing a context as the options, as in await create.getObject(ctx), is enough. Activities received in inboxes use this verifier by default, and objects fetched through their accessors inherit it, so a chained call such as (await create.getObject(ctx))?.getAttribution() verifies each portable object along the way. A lookup asks at most five gateways, falling back to the next when one fails or serves something that does not verify. Unsigned collections are accepted only from a gateway that the owner lists, and only for an actor's inbox, outbox, followers, following, and liked, as FEP-ef61 allows. fetchPortableMedia() retrieves the media of a portable object and returns it only if it matches the object's digestMultibase. See Dereferencing portable references and Default verifiers.
Software that does not understand ap: URIs can refer to a portable object by a compatible identifier, an HTTP(S) URL under a gateway's /.well-known/apgateway/ path such as https://gw.example/.well-known/apgateway/did:key:z6Mk.../actor. Some implementations, tootik among them, use compatible identifiers as the IDs of their portable actors.
Earlier versions of Fedify treated these URLs as ordinary web URLs and trusted them by the server that served them. Since anyone can serve a compatible identifier for any DID, https://evil.example/.well-known/apgateway/did:key:z6MkAlice/actor could act as Alice without a proof by Alice's DID. Fedify 2.4.0 treats a compatible identifier as the portable object it stands for, as FEP-ef61 requires. Inboxes reject an activity from an actor whose ID is a compatible identifier with 401 Unauthorized unless it has a valid Object Integrity Proof by the actor's DID, and there is no option to turn this off. Actors that publish compatible identifiers without signing them are no longer accepted, and neither are those whose DIDs use key types that Fedify cannot verify yet. The change was tracked in #1093 and made in #1105.
Your own portable actors may still use compatible identifiers as their IDs, for software that rejects ap: IDs. Build them with toCompatibleEf61Id() before signing, and Fedify will treat them as portable wherever it serves or sends them. It also warns if one is malformed or does not point at the actor's first gateway. See Compatible identifiers as actor IDs.
Two choices in Fedify's profile differ from the current FEP text or rest on semantics that are not settled yet. Fedify keeps both for the 2.x series, and either may change in Fedify 3.0.
The first is the scheme. FEP-ef61 recommends ap: and canonicalizes ap+ef61: to it, while also warning that the recommendation may change to ap+ef61:, since these IDs are meant only for portable objects. Fedify accepts both, compares them as equal, and serializes ap+ef61:. Parsing a document normalizes its portable IRIs to ap+ef61:, so re-serializing an object that another implementation signed with ap: IDs breaks its proof. Keep the received JSON of such documents, and compare portable IDs with arePortableUrisEqual() rather than as strings. The discussion is in #826 and #828, and the reasoning is in Canonical ap+ef61: scheme.
The second concerns compound documents. When a signed Note sits inside a signed Create, the verifier has to know which part of the JSON each proof covers, and neither FEP-8b32 nor Verifiable Credential Data Integrity defines that yet; the question is open in w3c/vc-data-integrity#350. Fedify uses an interim map-local profile, worked out in #938. Each JSON map with a proof is its own secured document, each embedded portable object needs its own proof and its own @context, and proof sets and chains are not supported. The profile authenticates the exact JSON values each proof covers; it does not promise that a child's JSON-LD expansion inside its parent matches its expansion on its own. @silverpill, @dmitrizagidulin, and @by_caballero reviewed the proposal in that thread, and @silverpill reproduced its test vectors independently. See Map-local compound proofs.
Fedify does not implement FEP-ae97, the companion proposal for client-side signing. There are no endpoints for registering actors, submitting client-signed activities to a portable outbox, or gateway discovery, and no media upload and deletion endpoints under /.well-known/apgateway-media. Your own outbox listeners can still relay activities that a client signed, unchanged. Fedify also does not copy or reconcile actors, objects, or collections between gateways, so running an actor on several gateways means keeping their data in sync yourself. There is no workflow for rotating a DID's key or migrating an actor to another DID. Fedify resolves verification methods only for did:key DIDs with Ed25519 keys; other DID methods verify only if your document loader resolves them. Gateways must be plain origins, since the arbitrary gateway paths that FEP-ef61 discusses are not supported. The full list is in Not supported.
The profile was tested against tootik v0.25.4 and Mitra v5.10.0 in #1202. Fedify verifies tootik's portable actors, activities, and HTTP Signatures, and Mitra's gateway HTTP Signatures on activities that a client signed. Both accept the portable activities that Fedify sends. Two fixes came out of this testing. Fedify now reads a gateways list that tootik leaves unmapped in its JSON-LD context. It also serializes gateways as bare origins, because Mitra rejects a trailing slash. Fedify has not yet verified an actor or activity that Mitra signed itself, and Mastodon's handling of portable actors has not been tested. The FEP-ef61 interoperability section of FEDERATION.md records what was tested and how.
fedify lookup looks up portable objects by their ap: and ap+ef61: IDs, compatible identifiers, and the handles of portable actors, and it verifies their proofs. When no gateway returns an acceptable object, it says why. A new --gateway option on fedify lookup, fedify inbox, and fedify webfinger names the gateways to ask. fedify inbox --follow follows portable actors, and fedify webfinger accepts portable actor IDs. See Looking up portable objects in the CLI manual.
The mock federation and contexts in @fedify/testing support portable IDs, verification against fixture document loaders, gateway requests, and hashlink media, so tests can exercise portable objects without a live gateway. @fedify/lint's actor URI rules accept the portable IDs that the getPortable*Uri() methods build. @fedify/next now passes hashlink media requests to Fedify, a problem reported in #1149 and fixed in #1170; to have Next.js run the middleware for them, add { source: "/.well-known/apgateway/:path*" } to your matcher.
Fedify already runs activity delivery and inbox processing on queue workers, but applications usually have jobs of their own: digest emails, timeline rebuilds, link previews. Until now, those jobs could not use Fedify's queues, serialization, or retries. Fedify 2.4.0 opens the same machinery to application-defined tasks.
A task is defined once with defineTask() and enqueued from any Context with enqueueTask() or enqueueTaskMany(). Every task takes a Standard Schema, such as a Zod or Valibot schema, which types its payload. Fedify validates the payload when it is enqueued and again when a worker picks it up; the second check catches payloads from an earlier deployment that no longer match the schema:
import { createExponentialBackoffPolicy } from "@fedify/fedify";
import { z } from "zod";
const sendDigest = federation.defineTask("sendDigest", {
schema: z.object({ userId: z.string(), since: z.date() }),
handler: async (ctx, data) => {
// data is typed as { userId: string; since: Date }
await sendEmail(data.userId, await buildDigest(data.userId, data.since));
},
retryPolicy: createExponentialBackoffPolicy({ maxAttempts: 3 }),
});
// Anywhere with a Context:
await ctx.enqueueTask(sendDigest, { userId: "alice", since: new Date() }, {
delay: { minutes: 30 },
deduplicationKey: "digest:alice",
});
Fedify serializes payloads with devalue, so Date, Map, Set, URL, bigint, Temporal values, circular references, and Activity Vocabulary objects all survive the trip through any message queue backend. A Note in a payload comes back as a Note. Failed handlers are retried with exponential backoff by default, and queues with native retries handle retries themselves. Tasks fall back to the outbox queue unless you give them their own. Use the new task slot of the queue option or a per-task queue for that, and startQueue(ctxData, { queue: "task" }) runs a task-only worker. Setting taskQueueResolution: "strict" turns the fallback into an error.
A deduplicationKey deduplicates enqueues. The feature came from @julian's point in #206 that recurring tasks are hard to manage when you cannot tell whether one is already queued. A queue that declares nativeDeduplication enforces the key itself; otherwise, Fedify keeps a best-effort marker in the key–value store through cas(). Each task runs in a fedify.task span, and the queue metrics from 2.3.0 now include task queues. The Background tasks chapter covers routing, retries, deduplication, and the limitations; cron-style scheduling, result backends, and priorities are out of scope for now.
ChanHaeng Lee (@2chanhaeng) implemented the API: the core in #803, deduplication in #806, and observability in #812, following the plans in #797, #798, and #799. The work was merged in #923, closing #206, which had been open since February 2025.
Fedify 2.2.0 added client-to-server outbox listeners, but a C2S client also has to upload images and video before it can post them. That traffic does not go through POST /outbox. Fedify 2.4.0 supports the ActivityPub Media Upload extension, a SocialCG proposal last updated in 2017 that never reached W3C Recommendation status.
setMediaUploader() registers a multipart/form-data endpoint. Its callback receives the uploaded file and the object shell the client posted along with it. If the callback returns the created object, Fedify responds with 201 Created; if it returns a URL where the object will appear once processing finishes, Fedify responds with 202 Accepted:
federation
.setMediaUploader(
"/users/{identifier}/media",
async (ctx, identifier, file, object) => {
const stored = await uploadToStorage(file);
return new Image({
id: ctx.getObjectUri(Image, { uuid: stored.uuid }),
url: new URL(stored.publicUrl),
mediaType: file.type,
name: object.name,
});
},
)
.authorize(async (ctx, identifier) => {
const session = await verifyAccessToken(
ctx.request.headers.get("authorization"),
);
return session?.identifier === identifier;
});
The actor dispatcher advertises the endpoint itself by setting endpoints: new Endpoints({ uploadMedia: ctx.getMediaUploaderUri(identifier) }), the same way it places its inbox and outbox URIs. Without an authorize() hook, anyone who finds the endpoint can upload to it. For that reason, Fedify logs a warning when an uploader is registered without one, and when the returned object's URI does not match an object dispatcher or the actor does not advertise the endpoint. Four new @fedify/lint rules catch the same mistakes before they run. Fedify reads the upload into memory and sets no size limit of its own, so cap the request size at your reverse proxy. Storing the file and serving the object back are up to the application. See the Media upload chapter; the work is in #754 and #927.
Fedify decides several things about every inbox delivery: which signature mechanisms applied, which keys they tried, whether the actor owned the key, and what happened to the activity. Before this release, applications saw only part of that. A Linked Data Signature by a key the actor did not own was counted the same as a bad signature, and the key that verified a request was never handed to the application. The request in #1191 came from an effort to log these decisions for a debugging tool.
The new onRequestFinished() callback on inbox listeners receives an InboxRequestReport for every delivery to an inbox route, including rejected requests and requests that fail before verification:
federation
.setInboxListeners("/users/{identifier}/inbox", "/inbox")
.on(Follow, async (ctx, follow) => { /* ... */ })
.onRequestFinished(async (ctx, report) => {
await db.insertInboxLog({
inbox: report.inbox.kind, // "personal" | "shared" | "portable"
authentication: report.authentication.status,
outcome: report.outcome.type === "response"
? report.outcome.disposition // "processed" | "enqueued" | "duplicate" | ...
: `exception:${report.outcome.stage}`,
keyIds: report.attempts
.flatMap((attempt) => attempt.checks)
.flatMap((check) => check.triedKeys)
.map((key) => key.id?.href),
});
});
Each HTTP Signature, Linked Data Signature, or Object Integrity Proof check appears as an attempt with the actual keys it tried, including a stale cached key and its refreshed replacement. The report also carries the final authentication decision, such as actorKeyMismatch or proofPolicy, and the response or exception that ended the request. Fedify awaits the callback whether or not the trace is sampled. Errors it throws are logged without changing the response, and enqueued means the queue accepted the activity, not that a worker later processed it. Mock federations in @fedify/testing accept the same callback. See Observing inbox requests; the implementation is in #1201.
Actor dispatchers have been able to return a Tombstone since 2.2.0, but object dispatchers could only return null. A deleted post was answered with 404 Not Found, as if it had never existed, so applications that wanted 410 Gone, as ActivityPub recommends, needed their own route in front of Fedify.
Object dispatchers may now return a Tombstone, which Fedify serves with 410 Gone and the serialized tombstone after applying the authorization predicate:
federation.setObjectDispatcher(
Note,
"/users/{identifier}/notes/{id}",
async (ctx, values) => {
const post = await getPost(values.identifier, values.id);
if (post == null) return null;
if (post.deletedAt != null) {
return new Tombstone({
id: ctx.getObjectUri(Note, values),
formerType: Note,
deleted: post.deletedAt,
});
}
return new Note({ id: ctx.getObjectUri(Note, values), content: post.content });
},
);
The return type of ObjectDispatcher widens to TObject | Tombstone | null. RequestContext.getObject() still returns null for a tombstone unless you pass { tombstone: "passthrough" } or ask for the Tombstone or Object class itself. Dispatchers registered for Tombstone or Object that already returned tombstones now respond with 410 rather than 200. See Deleted objects; the change was proposed in #1112 and made in #1117.
Much of what Fedify fetches is chosen by the sender of an incoming request. Key IDs in signatures are one example, and the servers behind them can be slow on purpose. Several changes in 2.4.0 bound that work.
As #1130 points out, a request may carry several RFC 9421 signatures, and each one can make Fedify fetch a key from a URL of the sender's choosing. Fedify now verifies at most the first three, in Signature-Input order, and ignores the rest. A signature counts toward the limit even if it fails before its key is fetched, and a key named by several signatures is looked up once. The new FederationOptions.maxHttpSignatures option, added with the limit in #1166, changes it.
The built-in document loaders now give each call ten seconds by default, a limit proposed in #1131 and added in #1169. The time limit covers every redirect, alternate document, double-knocking retry, and the response body. A timed-out call throws a FetchError whose cause is a DOMException named "TimeoutError". Error responses are read in full before the loader throws, up to 1 MiB, so that a server cannot send a 404 header and then stall the body. FederationOptions.documentLoaderTimeout changes the limit or turns it off with null, and custom document loaders are not affected. Since #913, getDocumentLoader() also rejects HTML responses that do not advertise an ActivityPub alternate with a FetchError, rather than failing inside JSON.parse(), which #912 found cluttering error trackers.
Cached public keys and remembered HTTP Message Signatures specs used to live forever in the key–value store. #1017 reports a production Redis instance holding 194,424 public-key entries, about 165 MiB, for actors that had mostly stopped federating. Keys now expire after 30 days and specs after 90, configurable through FederationOptions.publicKeyTtl and FederationOptions.httpMessageSignaturesSpecTtl. Entries written by earlier versions have no expiry until they are next written. The Clearing legacy cache entries section shows how to remove them for Redis, PostgreSQL, and other stores. Heewon Chae (@heeoneie) implemented this in #1027, their first contribution to Fedify.
const federation = createFederation<void>({
kv,
maxHttpSignatures: 1, // default: 3
documentLoaderTimeout: { seconds: 5 }, // default: 10 seconds
publicKeyTtl: { days: 7 }, // default: 30 days
httpMessageSignaturesSpecTtl: { days: 30 }, // default: 90 days
});
Finally, verifyProof() now authenticates every option of an Ed25519 JCS proof except proofValue, including expires, domain, challenge, and nonce. It also rejects expired or malformed ones. Previously, a field such as expires could be added to an already signed proof without invalidating it. Jiwon Kwon (@z9mb1) spotted this in review of #968. Callers can now require a domain and challenge through VerifyProofOptions to prevent cross-domain or replayed use.
@fedify/adonisjs@fedify/adonisjs integrates Fedify with AdonisJS. Running node ace add @fedify/adonisjs registers a service provider that owns the federation's lifecycle and a server middleware that mounts it. It also scaffolds config/fedify.ts, start/federation.ts, and app/federation/main.ts. Controllers get a ctx.federation request context:
// app/federation/main.ts
import federation from "@fedify/adonisjs/services/builder";
import { Person } from "@fedify/vocab";
federation.setActorDispatcher("/actors/{identifier}", (ctx, identifier) =>
new Person({ id: ctx.getActorUri(identifier), preferredUsername: identifier }));
The middleware sits first in AdonisJS's server stack rather than its router stack. That way Fedify sees paths the router does not know, such as /.well-known/webfinger, and reads request bodies before the body parser consumes them. The package targets Node.js and is published to npm only. See the AdonisJS section of the integration guide.
Samuel Brinkmann (@sabrinkmann) contributed the package in #1006, closing #139, and built it on a prototype that Emelia Smith (@thisismissem) attached to that issue.
npm add @fedify/adonisjs
pnpm add @fedify/adonisjs
yarn add @fedify/adonisjs
@fedify/interaction-controlsGoToSocial interaction controls, FEP-044f quote authorization, and FEP-7aa9 featured collections share a request-and-authorization flow. A server asks to reply to, quote, or feature a post, and the author's server approves according to the post's interactionPolicy. @fedify/interaction-controls implements that flow for like, reply, announce, quote, and feature interactions. Each has a helper, such as likeInteraction or quoteInteraction, that creates and verifies requests and authorizations, evaluates policies, and recognizes interactions that arrived without a request:
import { likeInteraction } from "@fedify/interaction-controls";
// When a LikeRequest arrives:
const verified = await likeInteraction.verifyRequest(ctx, { request });
if (verified.verified) {
const decision = await likeInteraction.evaluatePolicy(ctx, {
subject: note,
requester: request.actorId!,
});
if (decision.result === "automatic") { // or "manual" or "denied"
const authorization = likeInteraction.createAuthorization({
id: new URL("https://example.com/authorizations/1"),
attributedTo: owner,
interactingObject: like,
interactionTarget: note,
});
// ...
}
}
The package does not install inbox listeners or store anything; the application decides where requests and authorizations live and when a human needs to approve. Verification fails closed. Without a policy, likes, replies, and announces are approved automatically, while quotes and features are denied. @fedify/vocab gains the FEP-7aa9 types this needs: FeaturedCollection, FeaturedItem, FeatureRequest, and FeatureAuthorization, along with actors' featuredCollections and InteractionPolicy.canFeature. Samuel Brinkmann asked for them in #810 once Mastodon 4.6 shipped the FEP. See the Interaction controls chapter; the work is in #914 and #929.
npm add @fedify/interaction-controls
pnpm add @fedify/interaction-controls
yarn add @fedify/interaction-controls
deno add jsr:@fedify/interaction-controls
bun add @fedify/interaction-controls
@fedify/netlify@fedify/netlify lets a Fedify application run on Netlify Functions without an external database or queue. NetlifyMessageQueue submits jobs as Netlify Async Workloads events, and createNetlifyQueueHandler() turns a Function into their consumer, with delayed delivery, native retries, and per-key FIFO ordering. NetlifyBlobsKvStore is a key–value store on Netlify Blobs with expiration, prefix listing, and compare-and-swap:
import {
createNetlifyQueueHandler,
NetlifyBlobsKvStore,
NetlifyMessageQueue,
} from "@fedify/netlify";
import { AsyncWorkloadsClient } from "@netlify/async-workloads";
import { getStore } from "@netlify/blobs";
export const kv = new NetlifyBlobsKvStore(
getStore({ name: "fedify", consistency: "strong" }),
);
export const queue = new NetlifyMessageQueue({
client: new AsyncWorkloadsClient(),
orderingKv: kv,
});
// netlify/functions/fedify-queue.ts
export default createNetlifyQueueHandler({
queue,
maxRetries: 4,
federation: () => builder.build({ kv, queue, manuallyStartQueue: true }),
});
The key–value store exists because of cost. #1010 describes a static blog whose ActivityPub traffic kept a Netlify Database from ever idling, which used most of the account's monthly credits. Jiwon Kwon contributed NetlifyBlobsKvStore in #1029, and the queue landed in #934. See Netlify Functions in the deployment guide.
npm add @fedify/netlify
pnpm add @fedify/netlify
yarn add @fedify/netlify
deno add jsr:@fedify/netlify
bun add @fedify/netlify
@fedify/pglite@fedify/pglite provides PgliteKvStore, a KvStore on an embedded PGlite database that you create and pass in. It uses the same schema as PostgresKvStore. It is meant for a single PGlite instance in a single runtime isolate, so it does not include a message queue: PGlite does not share data between processes.
import { PGlite } from "@electric-sql/pglite";
import { PgliteKvStore } from "@fedify/pglite";
const federation = createFederation({
kv: new PgliteKvStore(new PGlite("./data/fedify")),
});
ChanHaeng Lee contributed the package in #1020, together with testKvStore() in @fedify/testing. testKvStore() is a conformance suite for KvStore implementations, the counterpart to testMessageQueue(). See PgliteKvStore.
npm add @fedify/pglite
pnpm add @fedify/pglite
yarn add @fedify/pglite
deno add jsr:@fedify/pglite
bun add @fedify/pglite
Portable inbox forwarding, task deduplication, Netlify queue ordering, and the delivery circuit breaker all need an atomic compare-and-swap on the key–value store. PostgresKvStore now implements cas(), added in #934, and so does RedisKvStore, requested in #1163 and added in #1167. The Redis version uses a single-key Lua script, which works on standalone Redis and Redis Cluster. Before this, the circuit breaker fell back to racy updates on Redis.
Both come with conditions. RedisKvStore needs Redis to permit EVAL, and custom codecs must encode equal values identically. PostgresKvStore now creates logged tables by default, because losing sequence state in crash recovery could block queued events for good. It migrates existing unlogged tables when it initializes. That one-time migration rewrites the table and locks it exclusively, so schedule the upgrade accordingly if the table is large or busy; unlogged: true keeps the old behavior.
fedify init and the CLIfedify init can now scaffold a SvelteKit project, contributed by Jang Hanarae (@menele) in #971. Jang Hanarae also added a test task to generated projects in #990. It starts the app, waits for it to be ready, and checks that it resolves a local actor, which gives new projects a smoke test to run after scaffolding and after later changes. Before generating anything, fedify init now checks that the selected Deno, Bun, or Node.js meets Fedify's minimum version, or a framework's higher one, such as Astro's Node.js 22.12. It fails with a clear error in non-interactive mode, and in interactive mode it disables the affected package managers. Lee Jeongmin (@userjmmm) contributed the check in #981. Astro projects now use Astro 7, and since #936 @fedify/astro is tested against Astro 5, 6, and 7 with real builds on Node.js, Deno, and Bun, as #931 asked.
fedify tunnel gains fedify.com.es, a tunneling service run by the Fedify project that needs no account; the CLI pins its SSH host key and refuses a mismatched server before exposing a local port. localhost.run is removed in the same change, #940, because the service no longer exists. fedify lookup now times out each request after ten seconds when -T/--timeout is not given, and the option now also bounds the wait for a DNS lookup. The CLI moves to Optique 1.3.2 in #1197, which handles attached values like --timeout=30 consistently, gives better suggestions for mistyped options, and no longer hides errors for known options behind positional arguments before --.
@fedify/lint gains several rules beyond the media upload ones mentioned above. actor-preferred-username-required, requested in #895 and added in #1022, warns when an actor dispatcher returns an actor without preferredUsername. outbox-listener-delivery-not-awaited, from #1057 and #1067, reports an outbox listener that calls ctx.sendActivity() or ctx.forwardActivity() and drops the promise. On Cloudflare Workers, such an activity may never leave, because pending work is discarded once the response returns. await, return, Promise.all(), Promise.allSettled(), and waitUntil() count as handling the promise, while void, Promise.race(), and Promise.any() are accepted as a deliberate choice not to wait. The rule is a warning in the ESLint recommended configuration and an error in strict. Oxlint users enable it by name, and it is not available in Deno Lint.
In #1050, which resolves #900, the existing outbox-listener-delivery-required rule began following control flow instead of scanning the listener's source as text. It reports a listener whose only delivery calls sit in a dead branch, after an unconditional return or throw, or in a function that is never used, and it stays quiet when it cannot tell. Jae-Hyuk-Jang (@jhyuk) contributed this change and both new rules. archie (@ArchieTansaria) then taught both delivery rules about statically false loops and loop binding patterns in #1088. See the linting guide.
@fedify/vocab supports the FEP-22cd draft for translations. A Translation records a translated version's language, translators, source object, and source review time, and the new Object.translations property attaches them to a post without creating separate posts. See Translation metadata. (#1037, #1038)@fedify/vocab-tools gains an extraContext property option, which adds a JSON-LD context only when its terms are used, and a trustEmbeddedObjects type option. (#1037, #1038)suppressError: true handles are now logged as warnings rather than errors, so that they no longer show up as application errors. Contributed by Jae Hui Hong (@axz0612). (#933, #1035)enqueueMany() makes separate sends can set MessageQueue.atomicEnqueueMany to false. Fedify then rejects a multi-message batch governed by one deduplicationKey up front, since a partial send followed by a retry could enqueue duplicates. (#930, #934)Temporal bundle temporal-polyfill instead of requiring @js-temporal/polyfill at runtime, and type declarations rely on the standard esnext.temporal lib. (#823, #925)Accept header now includes the ActivityStreams profile on the JSON-LD media type, as ActivityPub and FEP-ef61 gateways require. (#834, #1077)fedify lookup --recurse reporting a timeout or other network failure as a possibly private object; it now reports the actual cause. (#1156, #1199)-p/--allow-private-address option of fedify webfinger being ignored. (#1156, #1199)outbox-listener-delivery-required and outbox-listener-delivery-not-awaited losing track of an object's functions when a nested helper assigns another property of that object. (#1125, #1140)npm add @fedify/fedify@2.4.0
pnpm add @fedify/fedify@2.4.0
yarn add @fedify/fedify@2.4.0
deno add jsr:@fedify/fedify@2.4.0
bun add @fedify/fedify@2.4.0
Update the other @fedify/* packages you use to 2.4.0 at the same time. To update the CLI, run npm install -g @fedify/cli@2.4.0, or see the installation instructions for other package managers.
Most applications need no code changes, but these changes in behavior may affect you:
401 Unauthorized unless they carry a valid Object Integrity Proof by the actor's DID. Keys at compatible identifiers, and keys at ordinary URLs that claim a portable owner, are no longer trusted by origin.Context as their options, such as await create.getObject(ctx), now verify the portable objects they dereference, including references to compatible identifiers from objects with ordinary HTTP(S) IDs./.well-known/apgateway/, and Context.sendActivity() may reject an activity that embeds others' portable objects. Next.js applications need the matcher shown above to serve hashlink media.maxHttpSignatures if you need more.fedify lookup does too.Tombstone or Object that return tombstones now respond with 410 Gone instead of 200 OK.PostgresKvStore migrates existing unlogged tables to logged tables on initialization, which locks the table.RedisKvStore needs EVAL permission for cas().Context and RequestContext interfaces need the new getPortable*Uri() methods and isSignedByAudience().fedify tunnel no longer supports localhost.run.Thanks to this release's contributors:
@fedify/adonisjs, and the request for FEP-7aa9 vocabularytest task in fedify initfedify initactor-preferred-username-required and outbox-listener-delivery-not-awaited lint rules, and control-flow analysis for outbox-listener-delivery-requiredChanHaeng Lee (@2chanhaeng) and Jiwon Kwon (@z9mb1) joined as maintainers after 2.3.0. ChanHaeng built the background task API and @fedify/pglite, Jiwon built NetlifyBlobsKvStore and rewrote the relay documentation, and both reviewed much of the rest of this release.
Many people added tests, documentation, and infrastructure during this cycle, often starting from a good first issue: Seoa Yoon (@bananamilk452), Lim Kyoujin (@dktsudgg), @m2nsp, Cho Seongmin (@seongmin36), Safal Shrestha (@Safal-Shrestha-SS), Jo Youngjae (@jaonz6057), @kecan0406, Loi Nguyen (@lntutor), Oh Taejun (@0w0), Yerin Park (@dstforye), Lee Dogeon (@moreal), and Sae Jin Kim (@siliconsjang).
The FEP-ef61 work owes a lot to people outside the project. @anotherdoesnm opened the request in #288. @silverpill, the author of FEP-ef61 and FEP-ae97, clarified the gateway trust model, reviewed the compound proof profile, and checked its test vectors. @dmitrizagidulin and @by_caballero weighed in on the profile's JSON-LD questions in #938. Testing against tootik and Mitra led to two of the interoperability fixes in this release.
See CHANGES.md for the complete changelog.
github.com
ActivityPub server framework in TypeScript. Contribute to fedify-dev/fedify development by creating an account on GitHub.
Fedify 2.4.0 is out.
The main addition is support for FEP-ef61 portable objects. A Fedify server can now host actors whose identity is a DID instead of a domain, serve their signed objects through /.well-known/apgateway/, and verify the portable objects of other servers. I tested it against tootik and Mitra, and wrote up where Fedify's profile differs from the FEP and what it leaves out.
The release also adds application-defined background tasks, the ActivityPub Media Upload extension, per-request inbox reports, 410 Gone for deleted objects, and four new packages (AdonisJS, Netlify, PGlite, and interaction controls).
Some defaults changed, so please read the upgrading section before you update, especially if you federate with software that uses FEP-ef61 compatible identifiers.
Release notes: https://hackers.pub/@fedify/2026/fedify-2-4.
Thanks to everyone who contributed to this cycle, and to @silverpill for patient answers about the FEP.
hackers.pub
Fedify is a TypeScript framework for building ActivityPub servers. It implements federation details such as HTTP Signatures, JSON-LD processing, WebFinger, inbox and outbox routing, and activity delivery.
Fedify 2.4.0 is a minor release with an unusually long changelog. The main addition is support for FEP-ef61 portable objects: a Fedify server can now host actors whose identity is a DID rather than a domain, serve and verify their signed objects, and consume the portable objects of other servers. The release also adds an API for application-defined background tasks, the ActivityPub Media Upload extension, a hook that reports how every inbox request was authenticated and handled, and 410 Gone tombstones for deleted objects. Several defaults change to limit what a hostile peer can make Fedify do. Four new packages ship with this version: @fedify/adonisjs, @fedify/interaction-controls, @fedify/netlify, and @fedify/pglite.
Most applications can upgrade without code changes, but a few of the new defaults are behavior changes. They are listed in the Upgrading section at the end.
An ActivityPub object's ID is an HTTP(S) URL on its server's domain. If that server shuts down, its IDs stop resolving, and an actor that moves elsewhere gets a new ID that everyone has to follow again. FEP-ef61, written by @silverpill of Mitra, proposes IDs that do not name a server at all. A portable ID is an ap: URI whose authority is a DID:
ap://did:key:z6MkrJVnaZkeFzdQyMZu1cgjg7k1pZZ6pvBQ7XJPt4swbTQ2/actor
The same object can be stored on several servers, called gateways, each of which serves it under /.well-known/apgateway/. Because no server owns the ID, a portable actor, activity, or object is authenticated only by its FEP-8b32 Object Integrity Proof, made with a key of the DID in its ID. A portable actor lists its gateways in a gateways property, and other servers deliver to its inbox through them.
Fedify 2.4.0 implements a profile of FEP-ef61 for gateways and consumers of portable objects. It makes two deliberate choices where the FEP or the specifications it rests on leave questions open, and it leaves out a few parts of the proposal; both are described below. The new Portable objects chapter of the manual walks through running portable actors from start to finish and then describes the profile as a whole, so that other implementers can tell what to expect from a Fedify server. The request came from @anotherdoesnm in #288, which tracked the work from a roadmap to the final pieces.
Portable objects are a foundation for nomadic identity, not nomadic identity itself. Fedify serves and verifies portable objects and forwards activities between gateways, but it does not copy actors or posts between servers, rotate keys, or move an actor to another DID. Full nomadic identity is still open in #413.
A portable actor starts with an Ed25519 key pair. Its DID is made from the public key with exportDidKey(), and whoever holds the private key can act as the actor, so store it with the actor and keep it safe:
import { generateCryptoKeyPair } from "@fedify/fedify";
import { exportDidKey } from "@fedify/vocab-runtime";
const { publicKey, privateKey } = await generateCryptoKeyPair("Ed25519");
const did = await exportDidKey(publicKey); // did:key:z6Mk...
// The verification method that the actor's proofs refer to:
const didKeyId = new URL(`${did}#${did.slice("did:key:".length)}`);
The actor dispatcher returns a portable actor as it would an ordinary one. Context.getPortableActorUri() and its siblings for inboxes, outboxes, collections, and objects build portable IDs from the dispatcher's paths and the DID, and signObject() signs the actor with the DID's key:
federation
.setActorDispatcher("/users/{identifier}", async (ctx, identifier) => {
const user = await findUser(identifier);
if (user == null) return null;
// A request through the gateway endpoint names the DID in its path,
// e.g., GET /.well-known/apgateway/did:key:z6Mk.../users/alice:
if (
ctx.portableRequest != null &&
ctx.portableRequest.authority !== user.did
) {
return null;
}
// This server's gateway keys for the actor (see below):
const keys = await ctx.getActorKeyPairs(identifier);
return await signObject(
new Person({
id: ctx.getPortableActorUri(identifier, user.did), // ap+ef61://did:key:z6Mk.../users/alice
preferredUsername: user.username,
inbox: ctx.getPortableInboxUri(identifier, user.did),
outbox: ctx.getPortableOutboxUri(identifier, user.did),
followers: ctx.getPortableFollowersUri(identifier, user.did),
gateways: [new URL("https://example.com")],
publicKeys: keys.map((key) => key.cryptographicKey),
assertionMethods: keys.map((key) => key.multikey),
}),
user.didKeyPair.privateKey,
new URL(`${user.did}#${user.did.slice("did:key:".length)}`),
);
})
.setKeyPairsDispatcher(async (ctx, identifier) => {
const user = await findUser(identifier);
// This server's own key pair for the actor, not the DID's:
return user == null ? [] : [user.gatewayKeyPair];
})
.mapPortableActorId(async (ctx, identifier) => {
const user = await findUser(identifier);
return user == null ? null : ctx.getPortableActorUri(identifier, user.did);
});
With this dispatcher, Fedify serves the actor at its compatible identifier, https://example.com/.well-known/apgateway/did:key:z6Mk.../users/alice, which is how other servers retrieve it. It serves it through WebFinger as @alice@example.com, since the handle's domain comes from the first gateway in gateways, and at its ordinary URL as before. On the gateway endpoint, Fedify serves a document only if its ID is the requested portable ID and it carries a proof made with a key of that DID.
The key pairs dispatcher and the new ActorCallbackSetters.mapPortableActorId() give the actor gateway keys. These are the server's own keys for the actor, and they sign only the HTTP requests the server makes on the actor's behalf, such as deliveries. They never sign the actor's activities or objects; only the DID's key does that. The actor document lists them under assertionMethods, as FEP-521a describes, so that other servers can verify the HTTP Signatures. See Dispatching the actor and Gateway keys of portable actors for the details.
Object and collection dispatchers serve portable objects and collections through the gateway endpoint in the same way, routed by the path after the DID. Two rules protect the actor's data. A non-public portable object is served only if the dispatcher has an authorization predicate, because FEP-ef61 forbids a gateway to show it to anyone outside its audience; the new RequestContext.isSignedByAudience() helps to write one. Without a predicate, a request for such an object gets 404 Not Found, as though the gateway did not store it. A deleted object is served as a signed Tombstone with 410 Gone. Resources that portable objects refer to by SHA-256 hashlinks, such as GET /.well-known/apgateway/hl:zQm..., are served by the new Federatable.setHashlinkMediaDispatcher().
A portable actor's activities have to be signed by its DID, which Fedify's key pairs dispatcher does not know about. Sign them with signObject() before sending, and pass normalizeExistingProofs: true so that the activity goes out in the form its proof covers. An activity that embeds a portable object, such as the Note of a Create, is signed inside out:
const actor = withGatewayHints(
ctx.getPortableActorUri(user.identifier, user.did),
[new URL("https://example.com")],
); // ap+ef61://did:key:z6Mk.../users/alice?@gateway=https%3A%2F%2Fexample.com
const note = await signObject(
new Note({
id: ctx.getPortableObjectUri(
Note,
{ identifier: user.identifier, id: crypto.randomUUID() },
user.did,
),
attribution: actor,
to: PUBLIC_COLLECTION,
content: "Hello, world!",
}),
user.didKeyPair.privateKey,
didKeyId,
);
const create = await signObject(
new Create({
id: ctx.getPortableObjectUri(
Note,
{ identifier: user.identifier, id: crypto.randomUUID() },
user.did,
),
actor,
to: PUBLIC_COLLECTION,
object: note,
}),
user.didKeyPair.privateKey,
didKeyId,
);
await ctx.sendActivity(
{ identifier: user.identifier },
"followers",
create,
{ normalizeExistingProofs: true },
);
This example depends on a fix to signObject() that is useful outside FEP-ef61 as well. Previously, a signed object assigned to a typed parent was rebuilt under the parent's JSON-LD context when the parent was serialized. The child kept its proofValue but lost the document its proof covered, so it no longer verified on its own. signObject() now captures the exact document it signed, and nested serialization embeds that document verbatim. The captured document is a snapshot: clone() does not carry it, so sign a clone again if it has to be embedded. The design is in #1044 and the fix in #1051.
Context.sendActivity() refuses a portable activity that is unsigned, signed by the wrong key, or carries a Linked Data Signature, and throws a TypeError before anything is queued. It also refuses a few shapes of compound document that Fedify's own inboxes would reject, such as a received signed Follow embedded in an Accept; refer to such an object by its ID instead. Deliveries to portable inboxes go through the recipient's gateways, or else the @gateway hints on the inbox, trying at most five of them until one accepts. Before this release, they failed with UrlError: Unsupported protocol: ap+ef61:. The rules are in Delivering to portable actors and Producing a compound document.
On the receiving side, a portable inbox is reached as POST /.well-known/apgateway/did:key:z6Mk.../users/alice/inbox and dispatched to the same inbox listeners as the ordinary inbox. Context.parseUri() gains a portable option, and the parsed result has the DID in a new authority property to check against the one you store. Fedify accepts an activity of a portable actor, or one with a portable ID, only if it has a valid proof by that DID; neither an HTTP Signature nor a Linked Data Signature authenticates it. Each portable object embedded in the activity is verified against its own proof before dispatch. An accepted activity that carries its own proof is forwarded to the actor's other gateways at most once, as FEP-ef61 recommends. Forwarding needs a key–value store that supports cas(), and the FederationOptions.portableInboxForwarding option tunes it. See Portable inboxes and Forwarding to other gateways.
Applications that never host a portable actor still meet portable objects from other servers. Context.lookupObject() resolves portable IDs, compatible identifiers, and the fediverse handles of portable actors, and returns portable objects only if they verify:
const actor = await ctx.lookupObject("@alice@example.com");
// A portable ID without @gateway hints needs gateways to ask:
const note = await ctx.lookupObject("ap://did:key:z6Mk.../notes/1", {
gateways: ["https://example.com"],
});
Property accessors fetch portable references through gateways as well, and they verify what they fetch with the new verifyPortableObject option. Context has a matching verifyPortableObject property, so passing a context as the options, as in await create.getObject(ctx), is enough. Activities received in inboxes use this verifier by default, and objects fetched through their accessors inherit it, so a chained call such as (await create.getObject(ctx))?.getAttribution() verifies each portable object along the way. A lookup asks at most five gateways, falling back to the next when one fails or serves something that does not verify. Unsigned collections are accepted only from a gateway that the owner lists, and only for an actor's inbox, outbox, followers, following, and liked, as FEP-ef61 allows. fetchPortableMedia() retrieves the media of a portable object and returns it only if it matches the object's digestMultibase. See Dereferencing portable references and Default verifiers.
Software that does not understand ap: URIs can refer to a portable object by a compatible identifier, an HTTP(S) URL under a gateway's /.well-known/apgateway/ path such as https://gw.example/.well-known/apgateway/did:key:z6Mk.../actor. Some implementations, tootik among them, use compatible identifiers as the IDs of their portable actors.
Earlier versions of Fedify treated these URLs as ordinary web URLs and trusted them by the server that served them. Since anyone can serve a compatible identifier for any DID, https://evil.example/.well-known/apgateway/did:key:z6MkAlice/actor could act as Alice without a proof by Alice's DID. Fedify 2.4.0 treats a compatible identifier as the portable object it stands for, as FEP-ef61 requires. Inboxes reject an activity from an actor whose ID is a compatible identifier with 401 Unauthorized unless it has a valid Object Integrity Proof by the actor's DID, and there is no option to turn this off. Actors that publish compatible identifiers without signing them are no longer accepted, and neither are those whose DIDs use key types that Fedify cannot verify yet. The change was tracked in #1093 and made in #1105.
Your own portable actors may still use compatible identifiers as their IDs, for software that rejects ap: IDs. Build them with toCompatibleEf61Id() before signing, and Fedify will treat them as portable wherever it serves or sends them. It also warns if one is malformed or does not point at the actor's first gateway. See Compatible identifiers as actor IDs.
Two choices in Fedify's profile differ from the current FEP text or rest on semantics that are not settled yet. Fedify keeps both for the 2.x series, and either may change in Fedify 3.0.
The first is the scheme. FEP-ef61 recommends ap: and canonicalizes ap+ef61: to it, while also warning that the recommendation may change to ap+ef61:, since these IDs are meant only for portable objects. Fedify accepts both, compares them as equal, and serializes ap+ef61:. Parsing a document normalizes its portable IRIs to ap+ef61:, so re-serializing an object that another implementation signed with ap: IDs breaks its proof. Keep the received JSON of such documents, and compare portable IDs with arePortableUrisEqual() rather than as strings. The discussion is in #826 and #828, and the reasoning is in Canonical ap+ef61: scheme.
The second concerns compound documents. When a signed Note sits inside a signed Create, the verifier has to know which part of the JSON each proof covers, and neither FEP-8b32 nor Verifiable Credential Data Integrity defines that yet; the question is open in w3c/vc-data-integrity#350. Fedify uses an interim map-local profile, worked out in #938. Each JSON map with a proof is its own secured document, each embedded portable object needs its own proof and its own @context, and proof sets and chains are not supported. The profile authenticates the exact JSON values each proof covers; it does not promise that a child's JSON-LD expansion inside its parent matches its expansion on its own. @silverpill, @dmitrizagidulin, and @by_caballero reviewed the proposal in that thread, and @silverpill reproduced its test vectors independently. See Map-local compound proofs.
Fedify does not implement FEP-ae97, the companion proposal for client-side signing. There are no endpoints for registering actors, submitting client-signed activities to a portable outbox, or gateway discovery, and no media upload and deletion endpoints under /.well-known/apgateway-media. Your own outbox listeners can still relay activities that a client signed, unchanged. Fedify also does not copy or reconcile actors, objects, or collections between gateways, so running an actor on several gateways means keeping their data in sync yourself. There is no workflow for rotating a DID's key or migrating an actor to another DID. Fedify resolves verification methods only for did:key DIDs with Ed25519 keys; other DID methods verify only if your document loader resolves them. Gateways must be plain origins, since the arbitrary gateway paths that FEP-ef61 discusses are not supported. The full list is in Not supported.
The profile was tested against tootik v0.25.4 and Mitra v5.10.0 in #1202. Fedify verifies tootik's portable actors, activities, and HTTP Signatures, and Mitra's gateway HTTP Signatures on activities that a client signed. Both accept the portable activities that Fedify sends. Two fixes came out of this testing. Fedify now reads a gateways list that tootik leaves unmapped in its JSON-LD context. It also serializes gateways as bare origins, because Mitra rejects a trailing slash. Fedify has not yet verified an actor or activity that Mitra signed itself, and Mastodon's handling of portable actors has not been tested. The FEP-ef61 interoperability section of FEDERATION.md records what was tested and how.
fedify lookup looks up portable objects by their ap: and ap+ef61: IDs, compatible identifiers, and the handles of portable actors, and it verifies their proofs. When no gateway returns an acceptable object, it says why. A new --gateway option on fedify lookup, fedify inbox, and fedify webfinger names the gateways to ask. fedify inbox --follow follows portable actors, and fedify webfinger accepts portable actor IDs. See Looking up portable objects in the CLI manual.
The mock federation and contexts in @fedify/testing support portable IDs, verification against fixture document loaders, gateway requests, and hashlink media, so tests can exercise portable objects without a live gateway. @fedify/lint's actor URI rules accept the portable IDs that the getPortable*Uri() methods build. @fedify/next now passes hashlink media requests to Fedify, a problem reported in #1149 and fixed in #1170; to have Next.js run the middleware for them, add { source: "/.well-known/apgateway/:path*" } to your matcher.
Fedify already runs activity delivery and inbox processing on queue workers, but applications usually have jobs of their own: digest emails, timeline rebuilds, link previews. Until now, those jobs could not use Fedify's queues, serialization, or retries. Fedify 2.4.0 opens the same machinery to application-defined tasks.
A task is defined once with defineTask() and enqueued from any Context with enqueueTask() or enqueueTaskMany(). Every task takes a Standard Schema, such as a Zod or Valibot schema, which types its payload. Fedify validates the payload when it is enqueued and again when a worker picks it up; the second check catches payloads from an earlier deployment that no longer match the schema:
import { createExponentialBackoffPolicy } from "@fedify/fedify";
import { z } from "zod";
const sendDigest = federation.defineTask("sendDigest", {
schema: z.object({ userId: z.string(), since: z.date() }),
handler: async (ctx, data) => {
// data is typed as { userId: string; since: Date }
await sendEmail(data.userId, await buildDigest(data.userId, data.since));
},
retryPolicy: createExponentialBackoffPolicy({ maxAttempts: 3 }),
});
// Anywhere with a Context:
await ctx.enqueueTask(sendDigest, { userId: "alice", since: new Date() }, {
delay: { minutes: 30 },
deduplicationKey: "digest:alice",
});
Fedify serializes payloads with devalue, so Date, Map, Set, URL, bigint, Temporal values, circular references, and Activity Vocabulary objects all survive the trip through any message queue backend. A Note in a payload comes back as a Note. Failed handlers are retried with exponential backoff by default, and queues with native retries handle retries themselves. Tasks fall back to the outbox queue unless you give them their own. Use the new task slot of the queue option or a per-task queue for that, and startQueue(ctxData, { queue: "task" }) runs a task-only worker. Setting taskQueueResolution: "strict" turns the fallback into an error.
A deduplicationKey deduplicates enqueues. The feature came from @julian's point in #206 that recurring tasks are hard to manage when you cannot tell whether one is already queued. A queue that declares nativeDeduplication enforces the key itself; otherwise, Fedify keeps a best-effort marker in the key–value store through cas(). Each task runs in a fedify.task span, and the queue metrics from 2.3.0 now include task queues. The Background tasks chapter covers routing, retries, deduplication, and the limitations; cron-style scheduling, result backends, and priorities are out of scope for now.
ChanHaeng Lee (@2chanhaeng) implemented the API: the core in #803, deduplication in #806, and observability in #812, following the plans in #797, #798, and #799. The work was merged in #923, closing #206, which had been open since February 2025.
Fedify 2.2.0 added client-to-server outbox listeners, but a C2S client also has to upload images and video before it can post them. That traffic does not go through POST /outbox. Fedify 2.4.0 supports the ActivityPub Media Upload extension, a SocialCG proposal last updated in 2017 that never reached W3C Recommendation status.
setMediaUploader() registers a multipart/form-data endpoint. Its callback receives the uploaded file and the object shell the client posted along with it. If the callback returns the created object, Fedify responds with 201 Created; if it returns a URL where the object will appear once processing finishes, Fedify responds with 202 Accepted:
federation
.setMediaUploader(
"/users/{identifier}/media",
async (ctx, identifier, file, object) => {
const stored = await uploadToStorage(file);
return new Image({
id: ctx.getObjectUri(Image, { uuid: stored.uuid }),
url: new URL(stored.publicUrl),
mediaType: file.type,
name: object.name,
});
},
)
.authorize(async (ctx, identifier) => {
const session = await verifyAccessToken(
ctx.request.headers.get("authorization"),
);
return session?.identifier === identifier;
});
The actor dispatcher advertises the endpoint itself by setting endpoints: new Endpoints({ uploadMedia: ctx.getMediaUploaderUri(identifier) }), the same way it places its inbox and outbox URIs. Without an authorize() hook, anyone who finds the endpoint can upload to it. For that reason, Fedify logs a warning when an uploader is registered without one, and when the returned object's URI does not match an object dispatcher or the actor does not advertise the endpoint. Four new @fedify/lint rules catch the same mistakes before they run. Fedify reads the upload into memory and sets no size limit of its own, so cap the request size at your reverse proxy. Storing the file and serving the object back are up to the application. See the Media upload chapter; the work is in #754 and #927.
Fedify decides several things about every inbox delivery: which signature mechanisms applied, which keys they tried, whether the actor owned the key, and what happened to the activity. Before this release, applications saw only part of that. A Linked Data Signature by a key the actor did not own was counted the same as a bad signature, and the key that verified a request was never handed to the application. The request in #1191 came from an effort to log these decisions for a debugging tool.
The new onRequestFinished() callback on inbox listeners receives an InboxRequestReport for every delivery to an inbox route, including rejected requests and requests that fail before verification:
federation
.setInboxListeners("/users/{identifier}/inbox", "/inbox")
.on(Follow, async (ctx, follow) => { /* ... */ })
.onRequestFinished(async (ctx, report) => {
await db.insertInboxLog({
inbox: report.inbox.kind, // "personal" | "shared" | "portable"
authentication: report.authentication.status,
outcome: report.outcome.type === "response"
? report.outcome.disposition // "processed" | "enqueued" | "duplicate" | ...
: `exception:${report.outcome.stage}`,
keyIds: report.attempts
.flatMap((attempt) => attempt.checks)
.flatMap((check) => check.triedKeys)
.map((key) => key.id?.href),
});
});
Each HTTP Signature, Linked Data Signature, or Object Integrity Proof check appears as an attempt with the actual keys it tried, including a stale cached key and its refreshed replacement. The report also carries the final authentication decision, such as actorKeyMismatch or proofPolicy, and the response or exception that ended the request. Fedify awaits the callback whether or not the trace is sampled. Errors it throws are logged without changing the response, and enqueued means the queue accepted the activity, not that a worker later processed it. Mock federations in @fedify/testing accept the same callback. See Observing inbox requests; the implementation is in #1201.
Actor dispatchers have been able to return a Tombstone since 2.2.0, but object dispatchers could only return null. A deleted post was answered with 404 Not Found, as if it had never existed, so applications that wanted 410 Gone, as ActivityPub recommends, needed their own route in front of Fedify.
Object dispatchers may now return a Tombstone, which Fedify serves with 410 Gone and the serialized tombstone after applying the authorization predicate:
federation.setObjectDispatcher(
Note,
"/users/{identifier}/notes/{id}",
async (ctx, values) => {
const post = await getPost(values.identifier, values.id);
if (post == null) return null;
if (post.deletedAt != null) {
return new Tombstone({
id: ctx.getObjectUri(Note, values),
formerType: Note,
deleted: post.deletedAt,
});
}
return new Note({ id: ctx.getObjectUri(Note, values), content: post.content });
},
);
The return type of ObjectDispatcher widens to TObject | Tombstone | null. RequestContext.getObject() still returns null for a tombstone unless you pass { tombstone: "passthrough" } or ask for the Tombstone or Object class itself. Dispatchers registered for Tombstone or Object that already returned tombstones now respond with 410 rather than 200. See Deleted objects; the change was proposed in #1112 and made in #1117.
Much of what Fedify fetches is chosen by the sender of an incoming request. Key IDs in signatures are one example, and the servers behind them can be slow on purpose. Several changes in 2.4.0 bound that work.
As #1130 points out, a request may carry several RFC 9421 signatures, and each one can make Fedify fetch a key from a URL of the sender's choosing. Fedify now verifies at most the first three, in Signature-Input order, and ignores the rest. A signature counts toward the limit even if it fails before its key is fetched, and a key named by several signatures is looked up once. The new FederationOptions.maxHttpSignatures option, added with the limit in #1166, changes it.
The built-in document loaders now give each call ten seconds by default, a limit proposed in #1131 and added in #1169. The time limit covers every redirect, alternate document, double-knocking retry, and the response body. A timed-out call throws a FetchError whose cause is a DOMException named "TimeoutError". Error responses are read in full before the loader throws, up to 1 MiB, so that a server cannot send a 404 header and then stall the body. FederationOptions.documentLoaderTimeout changes the limit or turns it off with null, and custom document loaders are not affected. Since #913, getDocumentLoader() also rejects HTML responses that do not advertise an ActivityPub alternate with a FetchError, rather than failing inside JSON.parse(), which #912 found cluttering error trackers.
Cached public keys and remembered HTTP Message Signatures specs used to live forever in the key–value store. #1017 reports a production Redis instance holding 194,424 public-key entries, about 165 MiB, for actors that had mostly stopped federating. Keys now expire after 30 days and specs after 90, configurable through FederationOptions.publicKeyTtl and FederationOptions.httpMessageSignaturesSpecTtl. Entries written by earlier versions have no expiry until they are next written. The Clearing legacy cache entries section shows how to remove them for Redis, PostgreSQL, and other stores. Heewon Chae (@heeoneie) implemented this in #1027, their first contribution to Fedify.
const federation = createFederation<void>({
kv,
maxHttpSignatures: 1, // default: 3
documentLoaderTimeout: { seconds: 5 }, // default: 10 seconds
publicKeyTtl: { days: 7 }, // default: 30 days
httpMessageSignaturesSpecTtl: { days: 30 }, // default: 90 days
});
Finally, verifyProof() now authenticates every option of an Ed25519 JCS proof except proofValue, including expires, domain, challenge, and nonce. It also rejects expired or malformed ones. Previously, a field such as expires could be added to an already signed proof without invalidating it. Jiwon Kwon (@z9mb1) spotted this in review of #968. Callers can now require a domain and challenge through VerifyProofOptions to prevent cross-domain or replayed use.
@fedify/adonisjs@fedify/adonisjs integrates Fedify with AdonisJS. Running node ace add @fedify/adonisjs registers a service provider that owns the federation's lifecycle and a server middleware that mounts it. It also scaffolds config/fedify.ts, start/federation.ts, and app/federation/main.ts. Controllers get a ctx.federation request context:
// app/federation/main.ts
import federation from "@fedify/adonisjs/services/builder";
import { Person } from "@fedify/vocab";
federation.setActorDispatcher("/actors/{identifier}", (ctx, identifier) =>
new Person({ id: ctx.getActorUri(identifier), preferredUsername: identifier }));
The middleware sits first in AdonisJS's server stack rather than its router stack. That way Fedify sees paths the router does not know, such as /.well-known/webfinger, and reads request bodies before the body parser consumes them. The package targets Node.js and is published to npm only. See the AdonisJS section of the integration guide.
Samuel Brinkmann (@sabrinkmann) contributed the package in #1006, closing #139, and built it on a prototype that Emelia Smith (@thisismissem) attached to that issue.
npm add @fedify/adonisjs
pnpm add @fedify/adonisjs
yarn add @fedify/adonisjs
@fedify/interaction-controlsGoToSocial interaction controls, FEP-044f quote authorization, and FEP-7aa9 featured collections share a request-and-authorization flow. A server asks to reply to, quote, or feature a post, and the author's server approves according to the post's interactionPolicy. @fedify/interaction-controls implements that flow for like, reply, announce, quote, and feature interactions. Each has a helper, such as likeInteraction or quoteInteraction, that creates and verifies requests and authorizations, evaluates policies, and recognizes interactions that arrived without a request:
import { likeInteraction } from "@fedify/interaction-controls";
// When a LikeRequest arrives:
const verified = await likeInteraction.verifyRequest(ctx, { request });
if (verified.verified) {
const decision = await likeInteraction.evaluatePolicy(ctx, {
subject: note,
requester: request.actorId!,
});
if (decision.result === "automatic") { // or "manual" or "denied"
const authorization = likeInteraction.createAuthorization({
id: new URL("https://example.com/authorizations/1"),
attributedTo: owner,
interactingObject: like,
interactionTarget: note,
});
// ...
}
}
The package does not install inbox listeners or store anything; the application decides where requests and authorizations live and when a human needs to approve. Verification fails closed. Without a policy, likes, replies, and announces are approved automatically, while quotes and features are denied. @fedify/vocab gains the FEP-7aa9 types this needs: FeaturedCollection, FeaturedItem, FeatureRequest, and FeatureAuthorization, along with actors' featuredCollections and InteractionPolicy.canFeature. Samuel Brinkmann asked for them in #810 once Mastodon 4.6 shipped the FEP. See the Interaction controls chapter; the work is in #914 and #929.
npm add @fedify/interaction-controls
pnpm add @fedify/interaction-controls
yarn add @fedify/interaction-controls
deno add jsr:@fedify/interaction-controls
bun add @fedify/interaction-controls
@fedify/netlify@fedify/netlify lets a Fedify application run on Netlify Functions without an external database or queue. NetlifyMessageQueue submits jobs as Netlify Async Workloads events, and createNetlifyQueueHandler() turns a Function into their consumer, with delayed delivery, native retries, and per-key FIFO ordering. NetlifyBlobsKvStore is a key–value store on Netlify Blobs with expiration, prefix listing, and compare-and-swap:
import {
createNetlifyQueueHandler,
NetlifyBlobsKvStore,
NetlifyMessageQueue,
} from "@fedify/netlify";
import { AsyncWorkloadsClient } from "@netlify/async-workloads";
import { getStore } from "@netlify/blobs";
export const kv = new NetlifyBlobsKvStore(
getStore({ name: "fedify", consistency: "strong" }),
);
export const queue = new NetlifyMessageQueue({
client: new AsyncWorkloadsClient(),
orderingKv: kv,
});
// netlify/functions/fedify-queue.ts
export default createNetlifyQueueHandler({
queue,
maxRetries: 4,
federation: () => builder.build({ kv, queue, manuallyStartQueue: true }),
});
The key–value store exists because of cost. #1010 describes a static blog whose ActivityPub traffic kept a Netlify Database from ever idling, which used most of the account's monthly credits. Jiwon Kwon contributed NetlifyBlobsKvStore in #1029, and the queue landed in #934. See Netlify Functions in the deployment guide.
npm add @fedify/netlify
pnpm add @fedify/netlify
yarn add @fedify/netlify
deno add jsr:@fedify/netlify
bun add @fedify/netlify
@fedify/pglite@fedify/pglite provides PgliteKvStore, a KvStore on an embedded PGlite database that you create and pass in. It uses the same schema as PostgresKvStore. It is meant for a single PGlite instance in a single runtime isolate, so it does not include a message queue: PGlite does not share data between processes.
import { PGlite } from "@electric-sql/pglite";
import { PgliteKvStore } from "@fedify/pglite";
const federation = createFederation({
kv: new PgliteKvStore(new PGlite("./data/fedify")),
});
ChanHaeng Lee contributed the package in #1020, together with testKvStore() in @fedify/testing. testKvStore() is a conformance suite for KvStore implementations, the counterpart to testMessageQueue(). See PgliteKvStore.
npm add @fedify/pglite
pnpm add @fedify/pglite
yarn add @fedify/pglite
deno add jsr:@fedify/pglite
bun add @fedify/pglite
Portable inbox forwarding, task deduplication, Netlify queue ordering, and the delivery circuit breaker all need an atomic compare-and-swap on the key–value store. PostgresKvStore now implements cas(), added in #934, and so does RedisKvStore, requested in #1163 and added in #1167. The Redis version uses a single-key Lua script, which works on standalone Redis and Redis Cluster. Before this, the circuit breaker fell back to racy updates on Redis.
Both come with conditions. RedisKvStore needs Redis to permit EVAL, and custom codecs must encode equal values identically. PostgresKvStore now creates logged tables by default, because losing sequence state in crash recovery could block queued events for good. It migrates existing unlogged tables when it initializes. That one-time migration rewrites the table and locks it exclusively, so schedule the upgrade accordingly if the table is large or busy; unlogged: true keeps the old behavior.
fedify init and the CLIfedify init can now scaffold a SvelteKit project, contributed by Jang Hanarae (@menele) in #971. Jang Hanarae also added a test task to generated projects in #990. It starts the app, waits for it to be ready, and checks that it resolves a local actor, which gives new projects a smoke test to run after scaffolding and after later changes. Before generating anything, fedify init now checks that the selected Deno, Bun, or Node.js meets Fedify's minimum version, or a framework's higher one, such as Astro's Node.js 22.12. It fails with a clear error in non-interactive mode, and in interactive mode it disables the affected package managers. Lee Jeongmin (@userjmmm) contributed the check in #981. Astro projects now use Astro 7, and since #936 @fedify/astro is tested against Astro 5, 6, and 7 with real builds on Node.js, Deno, and Bun, as #931 asked.
fedify tunnel gains fedify.com.es, a tunneling service run by the Fedify project that needs no account; the CLI pins its SSH host key and refuses a mismatched server before exposing a local port. localhost.run is removed in the same change, #940, because the service no longer exists. fedify lookup now times out each request after ten seconds when -T/--timeout is not given, and the option now also bounds the wait for a DNS lookup. The CLI moves to Optique 1.3.2 in #1197, which handles attached values like --timeout=30 consistently, gives better suggestions for mistyped options, and no longer hides errors for known options behind positional arguments before --.
@fedify/lint gains several rules beyond the media upload ones mentioned above. actor-preferred-username-required, requested in #895 and added in #1022, warns when an actor dispatcher returns an actor without preferredUsername. outbox-listener-delivery-not-awaited, from #1057 and #1067, reports an outbox listener that calls ctx.sendActivity() or ctx.forwardActivity() and drops the promise. On Cloudflare Workers, such an activity may never leave, because pending work is discarded once the response returns. await, return, Promise.all(), Promise.allSettled(), and waitUntil() count as handling the promise, while void, Promise.race(), and Promise.any() are accepted as a deliberate choice not to wait. The rule is a warning in the ESLint recommended configuration and an error in strict. Oxlint users enable it by name, and it is not available in Deno Lint.
In #1050, which resolves #900, the existing outbox-listener-delivery-required rule began following control flow instead of scanning the listener's source as text. It reports a listener whose only delivery calls sit in a dead branch, after an unconditional return or throw, or in a function that is never used, and it stays quiet when it cannot tell. Jae-Hyuk-Jang (@jhyuk) contributed this change and both new rules. archie (@ArchieTansaria) then taught both delivery rules about statically false loops and loop binding patterns in #1088. See the linting guide.
@fedify/vocab supports the FEP-22cd draft for translations. A Translation records a translated version's language, translators, source object, and source review time, and the new Object.translations property attaches them to a post without creating separate posts. See Translation metadata. (#1037, #1038)@fedify/vocab-tools gains an extraContext property option, which adds a JSON-LD context only when its terms are used, and a trustEmbeddedObjects type option. (#1037, #1038)suppressError: true handles are now logged as warnings rather than errors, so that they no longer show up as application errors. Contributed by Jae Hui Hong (@axz0612). (#933, #1035)enqueueMany() makes separate sends can set MessageQueue.atomicEnqueueMany to false. Fedify then rejects a multi-message batch governed by one deduplicationKey up front, since a partial send followed by a retry could enqueue duplicates. (#930, #934)Temporal bundle temporal-polyfill instead of requiring @js-temporal/polyfill at runtime, and type declarations rely on the standard esnext.temporal lib. (#823, #925)Accept header now includes the ActivityStreams profile on the JSON-LD media type, as ActivityPub and FEP-ef61 gateways require. (#834, #1077)fedify lookup --recurse reporting a timeout or other network failure as a possibly private object; it now reports the actual cause. (#1156, #1199)-p/--allow-private-address option of fedify webfinger being ignored. (#1156, #1199)outbox-listener-delivery-required and outbox-listener-delivery-not-awaited losing track of an object's functions when a nested helper assigns another property of that object. (#1125, #1140)npm add @fedify/fedify@2.4.0
pnpm add @fedify/fedify@2.4.0
yarn add @fedify/fedify@2.4.0
deno add jsr:@fedify/fedify@2.4.0
bun add @fedify/fedify@2.4.0
Update the other @fedify/* packages you use to 2.4.0 at the same time. To update the CLI, run npm install -g @fedify/cli@2.4.0, or see the installation instructions for other package managers.
Most applications need no code changes, but these changes in behavior may affect you:
401 Unauthorized unless they carry a valid Object Integrity Proof by the actor's DID. Keys at compatible identifiers, and keys at ordinary URLs that claim a portable owner, are no longer trusted by origin.Context as their options, such as await create.getObject(ctx), now verify the portable objects they dereference, including references to compatible identifiers from objects with ordinary HTTP(S) IDs./.well-known/apgateway/, and Context.sendActivity() may reject an activity that embeds others' portable objects. Next.js applications need the matcher shown above to serve hashlink media.maxHttpSignatures if you need more.fedify lookup does too.Tombstone or Object that return tombstones now respond with 410 Gone instead of 200 OK.PostgresKvStore migrates existing unlogged tables to logged tables on initialization, which locks the table.RedisKvStore needs EVAL permission for cas().Context and RequestContext interfaces need the new getPortable*Uri() methods and isSignedByAudience().fedify tunnel no longer supports localhost.run.Thanks to this release's contributors:
@fedify/adonisjs, and the request for FEP-7aa9 vocabularytest task in fedify initfedify initactor-preferred-username-required and outbox-listener-delivery-not-awaited lint rules, and control-flow analysis for outbox-listener-delivery-requiredChanHaeng Lee (@2chanhaeng) and Jiwon Kwon (@z9mb1) joined as maintainers after 2.3.0. ChanHaeng built the background task API and @fedify/pglite, Jiwon built NetlifyBlobsKvStore and rewrote the relay documentation, and both reviewed much of the rest of this release.
Many people added tests, documentation, and infrastructure during this cycle, often starting from a good first issue: Seoa Yoon (@bananamilk452), Lim Kyoujin (@dktsudgg), @m2nsp, Cho Seongmin (@seongmin36), Safal Shrestha (@Safal-Shrestha-SS), Jo Youngjae (@jaonz6057), @kecan0406, Loi Nguyen (@lntutor), Oh Taejun (@0w0), Yerin Park (@dstforye), Lee Dogeon (@moreal), and Sae Jin Kim (@siliconsjang).
The FEP-ef61 work owes a lot to people outside the project. @anotherdoesnm opened the request in #288. @silverpill, the author of FEP-ef61 and FEP-ae97, clarified the gateway trust model, reviewed the compound proof profile, and checked its test vectors. @dmitrizagidulin and @by_caballero weighed in on the profile's JSON-LD questions in #938. Testing against tootik and Mitra led to two of the interoperability fixes in this release.
See CHANGES.md for the complete changelog.
github.com
ActivityPub server framework in TypeScript. Contribute to fedify-dev/fedify development by creating an account on GitHub.
Fedify 2.4.0 is out.
The main addition is support for FEP-ef61 portable objects. A Fedify server can now host actors whose identity is a DID instead of a domain, serve their signed objects through /.well-known/apgateway/, and verify the portable objects of other servers. I tested it against tootik and Mitra, and wrote up where Fedify's profile differs from the FEP and what it leaves out.
The release also adds application-defined background tasks, the ActivityPub Media Upload extension, per-request inbox reports, 410 Gone for deleted objects, and four new packages (AdonisJS, Netlify, PGlite, and interaction controls).
Some defaults changed, so please read the upgrading section before you update, especially if you federate with software that uses FEP-ef61 compatible identifiers.
Release notes: https://hackers.pub/@fedify/2026/fedify-2-4.
Thanks to everyone who contributed to this cycle, and to @silverpill for patient answers about the FEP.
hackers.pub
Fedify is a TypeScript framework for building ActivityPub servers. It implements federation details such as HTTP Signatures, JSON-LD processing, WebFinger, inbox and outbox routing, and activity delivery.
Fedify 2.4.0 is a minor release with an unusually long changelog. The main addition is support for FEP-ef61 portable objects: a Fedify server can now host actors whose identity is a DID rather than a domain, serve and verify their signed objects, and consume the portable objects of other servers. The release also adds an API for application-defined background tasks, the ActivityPub Media Upload extension, a hook that reports how every inbox request was authenticated and handled, and 410 Gone tombstones for deleted objects. Several defaults change to limit what a hostile peer can make Fedify do. Four new packages ship with this version: @fedify/adonisjs, @fedify/interaction-controls, @fedify/netlify, and @fedify/pglite.
Most applications can upgrade without code changes, but a few of the new defaults are behavior changes. They are listed in the Upgrading section at the end.
An ActivityPub object's ID is an HTTP(S) URL on its server's domain. If that server shuts down, its IDs stop resolving, and an actor that moves elsewhere gets a new ID that everyone has to follow again. FEP-ef61, written by @silverpill of Mitra, proposes IDs that do not name a server at all. A portable ID is an ap: URI whose authority is a DID:
ap://did:key:z6MkrJVnaZkeFzdQyMZu1cgjg7k1pZZ6pvBQ7XJPt4swbTQ2/actor
The same object can be stored on several servers, called gateways, each of which serves it under /.well-known/apgateway/. Because no server owns the ID, a portable actor, activity, or object is authenticated only by its FEP-8b32 Object Integrity Proof, made with a key of the DID in its ID. A portable actor lists its gateways in a gateways property, and other servers deliver to its inbox through them.
Fedify 2.4.0 implements a profile of FEP-ef61 for gateways and consumers of portable objects. It makes two deliberate choices where the FEP or the specifications it rests on leave questions open, and it leaves out a few parts of the proposal; both are described below. The new Portable objects chapter of the manual walks through running portable actors from start to finish and then describes the profile as a whole, so that other implementers can tell what to expect from a Fedify server. The request came from @anotherdoesnm in #288, which tracked the work from a roadmap to the final pieces.
Portable objects are a foundation for nomadic identity, not nomadic identity itself. Fedify serves and verifies portable objects and forwards activities between gateways, but it does not copy actors or posts between servers, rotate keys, or move an actor to another DID. Full nomadic identity is still open in #413.
A portable actor starts with an Ed25519 key pair. Its DID is made from the public key with exportDidKey(), and whoever holds the private key can act as the actor, so store it with the actor and keep it safe:
import { generateCryptoKeyPair } from "@fedify/fedify";
import { exportDidKey } from "@fedify/vocab-runtime";
const { publicKey, privateKey } = await generateCryptoKeyPair("Ed25519");
const did = await exportDidKey(publicKey); // did:key:z6Mk...
// The verification method that the actor's proofs refer to:
const didKeyId = new URL(`${did}#${did.slice("did:key:".length)}`);
The actor dispatcher returns a portable actor as it would an ordinary one. Context.getPortableActorUri() and its siblings for inboxes, outboxes, collections, and objects build portable IDs from the dispatcher's paths and the DID, and signObject() signs the actor with the DID's key:
federation
.setActorDispatcher("/users/{identifier}", async (ctx, identifier) => {
const user = await findUser(identifier);
if (user == null) return null;
// A request through the gateway endpoint names the DID in its path,
// e.g., GET /.well-known/apgateway/did:key:z6Mk.../users/alice:
if (
ctx.portableRequest != null &&
ctx.portableRequest.authority !== user.did
) {
return null;
}
// This server's gateway keys for the actor (see below):
const keys = await ctx.getActorKeyPairs(identifier);
return await signObject(
new Person({
id: ctx.getPortableActorUri(identifier, user.did), // ap+ef61://did:key:z6Mk.../users/alice
preferredUsername: user.username,
inbox: ctx.getPortableInboxUri(identifier, user.did),
outbox: ctx.getPortableOutboxUri(identifier, user.did),
followers: ctx.getPortableFollowersUri(identifier, user.did),
gateways: [new URL("https://example.com")],
publicKeys: keys.map((key) => key.cryptographicKey),
assertionMethods: keys.map((key) => key.multikey),
}),
user.didKeyPair.privateKey,
new URL(`${user.did}#${user.did.slice("did:key:".length)}`),
);
})
.setKeyPairsDispatcher(async (ctx, identifier) => {
const user = await findUser(identifier);
// This server's own key pair for the actor, not the DID's:
return user == null ? [] : [user.gatewayKeyPair];
})
.mapPortableActorId(async (ctx, identifier) => {
const user = await findUser(identifier);
return user == null ? null : ctx.getPortableActorUri(identifier, user.did);
});
With this dispatcher, Fedify serves the actor at its compatible identifier, https://example.com/.well-known/apgateway/did:key:z6Mk.../users/alice, which is how other servers retrieve it. It serves it through WebFinger as @alice@example.com, since the handle's domain comes from the first gateway in gateways, and at its ordinary URL as before. On the gateway endpoint, Fedify serves a document only if its ID is the requested portable ID and it carries a proof made with a key of that DID.
The key pairs dispatcher and the new ActorCallbackSetters.mapPortableActorId() give the actor gateway keys. These are the server's own keys for the actor, and they sign only the HTTP requests the server makes on the actor's behalf, such as deliveries. They never sign the actor's activities or objects; only the DID's key does that. The actor document lists them under assertionMethods, as FEP-521a describes, so that other servers can verify the HTTP Signatures. See Dispatching the actor and Gateway keys of portable actors for the details.
Object and collection dispatchers serve portable objects and collections through the gateway endpoint in the same way, routed by the path after the DID. Two rules protect the actor's data. A non-public portable object is served only if the dispatcher has an authorization predicate, because FEP-ef61 forbids a gateway to show it to anyone outside its audience; the new RequestContext.isSignedByAudience() helps to write one. Without a predicate, a request for such an object gets 404 Not Found, as though the gateway did not store it. A deleted object is served as a signed Tombstone with 410 Gone. Resources that portable objects refer to by SHA-256 hashlinks, such as GET /.well-known/apgateway/hl:zQm..., are served by the new Federatable.setHashlinkMediaDispatcher().
A portable actor's activities have to be signed by its DID, which Fedify's key pairs dispatcher does not know about. Sign them with signObject() before sending, and pass normalizeExistingProofs: true so that the activity goes out in the form its proof covers. An activity that embeds a portable object, such as the Note of a Create, is signed inside out:
const actor = withGatewayHints(
ctx.getPortableActorUri(user.identifier, user.did),
[new URL("https://example.com")],
); // ap+ef61://did:key:z6Mk.../users/alice?@gateway=https%3A%2F%2Fexample.com
const note = await signObject(
new Note({
id: ctx.getPortableObjectUri(
Note,
{ identifier: user.identifier, id: crypto.randomUUID() },
user.did,
),
attribution: actor,
to: PUBLIC_COLLECTION,
content: "Hello, world!",
}),
user.didKeyPair.privateKey,
didKeyId,
);
const create = await signObject(
new Create({
id: ctx.getPortableObjectUri(
Note,
{ identifier: user.identifier, id: crypto.randomUUID() },
user.did,
),
actor,
to: PUBLIC_COLLECTION,
object: note,
}),
user.didKeyPair.privateKey,
didKeyId,
);
await ctx.sendActivity(
{ identifier: user.identifier },
"followers",
create,
{ normalizeExistingProofs: true },
);
This example depends on a fix to signObject() that is useful outside FEP-ef61 as well. Previously, a signed object assigned to a typed parent was rebuilt under the parent's JSON-LD context when the parent was serialized. The child kept its proofValue but lost the document its proof covered, so it no longer verified on its own. signObject() now captures the exact document it signed, and nested serialization embeds that document verbatim. The captured document is a snapshot: clone() does not carry it, so sign a clone again if it has to be embedded. The design is in #1044 and the fix in #1051.
Context.sendActivity() refuses a portable activity that is unsigned, signed by the wrong key, or carries a Linked Data Signature, and throws a TypeError before anything is queued. It also refuses a few shapes of compound document that Fedify's own inboxes would reject, such as a received signed Follow embedded in an Accept; refer to such an object by its ID instead. Deliveries to portable inboxes go through the recipient's gateways, or else the @gateway hints on the inbox, trying at most five of them until one accepts. Before this release, they failed with UrlError: Unsupported protocol: ap+ef61:. The rules are in Delivering to portable actors and Producing a compound document.
On the receiving side, a portable inbox is reached as POST /.well-known/apgateway/did:key:z6Mk.../users/alice/inbox and dispatched to the same inbox listeners as the ordinary inbox. Context.parseUri() gains a portable option, and the parsed result has the DID in a new authority property to check against the one you store. Fedify accepts an activity of a portable actor, or one with a portable ID, only if it has a valid proof by that DID; neither an HTTP Signature nor a Linked Data Signature authenticates it. Each portable object embedded in the activity is verified against its own proof before dispatch. An accepted activity that carries its own proof is forwarded to the actor's other gateways at most once, as FEP-ef61 recommends. Forwarding needs a key–value store that supports cas(), and the FederationOptions.portableInboxForwarding option tunes it. See Portable inboxes and Forwarding to other gateways.
Applications that never host a portable actor still meet portable objects from other servers. Context.lookupObject() resolves portable IDs, compatible identifiers, and the fediverse handles of portable actors, and returns portable objects only if they verify:
const actor = await ctx.lookupObject("@alice@example.com");
// A portable ID without @gateway hints needs gateways to ask:
const note = await ctx.lookupObject("ap://did:key:z6Mk.../notes/1", {
gateways: ["https://example.com"],
});
Property accessors fetch portable references through gateways as well, and they verify what they fetch with the new verifyPortableObject option. Context has a matching verifyPortableObject property, so passing a context as the options, as in await create.getObject(ctx), is enough. Activities received in inboxes use this verifier by default, and objects fetched through their accessors inherit it, so a chained call such as (await create.getObject(ctx))?.getAttribution() verifies each portable object along the way. A lookup asks at most five gateways, falling back to the next when one fails or serves something that does not verify. Unsigned collections are accepted only from a gateway that the owner lists, and only for an actor's inbox, outbox, followers, following, and liked, as FEP-ef61 allows. fetchPortableMedia() retrieves the media of a portable object and returns it only if it matches the object's digestMultibase. See Dereferencing portable references and Default verifiers.
Software that does not understand ap: URIs can refer to a portable object by a compatible identifier, an HTTP(S) URL under a gateway's /.well-known/apgateway/ path such as https://gw.example/.well-known/apgateway/did:key:z6Mk.../actor. Some implementations, tootik among them, use compatible identifiers as the IDs of their portable actors.
Earlier versions of Fedify treated these URLs as ordinary web URLs and trusted them by the server that served them. Since anyone can serve a compatible identifier for any DID, https://evil.example/.well-known/apgateway/did:key:z6MkAlice/actor could act as Alice without a proof by Alice's DID. Fedify 2.4.0 treats a compatible identifier as the portable object it stands for, as FEP-ef61 requires. Inboxes reject an activity from an actor whose ID is a compatible identifier with 401 Unauthorized unless it has a valid Object Integrity Proof by the actor's DID, and there is no option to turn this off. Actors that publish compatible identifiers without signing them are no longer accepted, and neither are those whose DIDs use key types that Fedify cannot verify yet. The change was tracked in #1093 and made in #1105.
Your own portable actors may still use compatible identifiers as their IDs, for software that rejects ap: IDs. Build them with toCompatibleEf61Id() before signing, and Fedify will treat them as portable wherever it serves or sends them. It also warns if one is malformed or does not point at the actor's first gateway. See Compatible identifiers as actor IDs.
Two choices in Fedify's profile differ from the current FEP text or rest on semantics that are not settled yet. Fedify keeps both for the 2.x series, and either may change in Fedify 3.0.
The first is the scheme. FEP-ef61 recommends ap: and canonicalizes ap+ef61: to it, while also warning that the recommendation may change to ap+ef61:, since these IDs are meant only for portable objects. Fedify accepts both, compares them as equal, and serializes ap+ef61:. Parsing a document normalizes its portable IRIs to ap+ef61:, so re-serializing an object that another implementation signed with ap: IDs breaks its proof. Keep the received JSON of such documents, and compare portable IDs with arePortableUrisEqual() rather than as strings. The discussion is in #826 and #828, and the reasoning is in Canonical ap+ef61: scheme.
The second concerns compound documents. When a signed Note sits inside a signed Create, the verifier has to know which part of the JSON each proof covers, and neither FEP-8b32 nor Verifiable Credential Data Integrity defines that yet; the question is open in w3c/vc-data-integrity#350. Fedify uses an interim map-local profile, worked out in #938. Each JSON map with a proof is its own secured document, each embedded portable object needs its own proof and its own @context, and proof sets and chains are not supported. The profile authenticates the exact JSON values each proof covers; it does not promise that a child's JSON-LD expansion inside its parent matches its expansion on its own. @silverpill, @dmitrizagidulin, and @by_caballero reviewed the proposal in that thread, and @silverpill reproduced its test vectors independently. See Map-local compound proofs.
Fedify does not implement FEP-ae97, the companion proposal for client-side signing. There are no endpoints for registering actors, submitting client-signed activities to a portable outbox, or gateway discovery, and no media upload and deletion endpoints under /.well-known/apgateway-media. Your own outbox listeners can still relay activities that a client signed, unchanged. Fedify also does not copy or reconcile actors, objects, or collections between gateways, so running an actor on several gateways means keeping their data in sync yourself. There is no workflow for rotating a DID's key or migrating an actor to another DID. Fedify resolves verification methods only for did:key DIDs with Ed25519 keys; other DID methods verify only if your document loader resolves them. Gateways must be plain origins, since the arbitrary gateway paths that FEP-ef61 discusses are not supported. The full list is in Not supported.
The profile was tested against tootik v0.25.4 and Mitra v5.10.0 in #1202. Fedify verifies tootik's portable actors, activities, and HTTP Signatures, and Mitra's gateway HTTP Signatures on activities that a client signed. Both accept the portable activities that Fedify sends. Two fixes came out of this testing. Fedify now reads a gateways list that tootik leaves unmapped in its JSON-LD context. It also serializes gateways as bare origins, because Mitra rejects a trailing slash. Fedify has not yet verified an actor or activity that Mitra signed itself, and Mastodon's handling of portable actors has not been tested. The FEP-ef61 interoperability section of FEDERATION.md records what was tested and how.
fedify lookup looks up portable objects by their ap: and ap+ef61: IDs, compatible identifiers, and the handles of portable actors, and it verifies their proofs. When no gateway returns an acceptable object, it says why. A new --gateway option on fedify lookup, fedify inbox, and fedify webfinger names the gateways to ask. fedify inbox --follow follows portable actors, and fedify webfinger accepts portable actor IDs. See Looking up portable objects in the CLI manual.
The mock federation and contexts in @fedify/testing support portable IDs, verification against fixture document loaders, gateway requests, and hashlink media, so tests can exercise portable objects without a live gateway. @fedify/lint's actor URI rules accept the portable IDs that the getPortable*Uri() methods build. @fedify/next now passes hashlink media requests to Fedify, a problem reported in #1149 and fixed in #1170; to have Next.js run the middleware for them, add { source: "/.well-known/apgateway/:path*" } to your matcher.
Fedify already runs activity delivery and inbox processing on queue workers, but applications usually have jobs of their own: digest emails, timeline rebuilds, link previews. Until now, those jobs could not use Fedify's queues, serialization, or retries. Fedify 2.4.0 opens the same machinery to application-defined tasks.
A task is defined once with defineTask() and enqueued from any Context with enqueueTask() or enqueueTaskMany(). Every task takes a Standard Schema, such as a Zod or Valibot schema, which types its payload. Fedify validates the payload when it is enqueued and again when a worker picks it up; the second check catches payloads from an earlier deployment that no longer match the schema:
import { createExponentialBackoffPolicy } from "@fedify/fedify";
import { z } from "zod";
const sendDigest = federation.defineTask("sendDigest", {
schema: z.object({ userId: z.string(), since: z.date() }),
handler: async (ctx, data) => {
// data is typed as { userId: string; since: Date }
await sendEmail(data.userId, await buildDigest(data.userId, data.since));
},
retryPolicy: createExponentialBackoffPolicy({ maxAttempts: 3 }),
});
// Anywhere with a Context:
await ctx.enqueueTask(sendDigest, { userId: "alice", since: new Date() }, {
delay: { minutes: 30 },
deduplicationKey: "digest:alice",
});
Fedify serializes payloads with devalue, so Date, Map, Set, URL, bigint, Temporal values, circular references, and Activity Vocabulary objects all survive the trip through any message queue backend. A Note in a payload comes back as a Note. Failed handlers are retried with exponential backoff by default, and queues with native retries handle retries themselves. Tasks fall back to the outbox queue unless you give them their own. Use the new task slot of the queue option or a per-task queue for that, and startQueue(ctxData, { queue: "task" }) runs a task-only worker. Setting taskQueueResolution: "strict" turns the fallback into an error.
A deduplicationKey deduplicates enqueues. The feature came from @julian's point in #206 that recurring tasks are hard to manage when you cannot tell whether one is already queued. A queue that declares nativeDeduplication enforces the key itself; otherwise, Fedify keeps a best-effort marker in the key–value store through cas(). Each task runs in a fedify.task span, and the queue metrics from 2.3.0 now include task queues. The Background tasks chapter covers routing, retries, deduplication, and the limitations; cron-style scheduling, result backends, and priorities are out of scope for now.
ChanHaeng Lee (@2chanhaeng) implemented the API: the core in #803, deduplication in #806, and observability in #812, following the plans in #797, #798, and #799. The work was merged in #923, closing #206, which had been open since February 2025.
Fedify 2.2.0 added client-to-server outbox listeners, but a C2S client also has to upload images and video before it can post them. That traffic does not go through POST /outbox. Fedify 2.4.0 supports the ActivityPub Media Upload extension, a SocialCG proposal last updated in 2017 that never reached W3C Recommendation status.
setMediaUploader() registers a multipart/form-data endpoint. Its callback receives the uploaded file and the object shell the client posted along with it. If the callback returns the created object, Fedify responds with 201 Created; if it returns a URL where the object will appear once processing finishes, Fedify responds with 202 Accepted:
federation
.setMediaUploader(
"/users/{identifier}/media",
async (ctx, identifier, file, object) => {
const stored = await uploadToStorage(file);
return new Image({
id: ctx.getObjectUri(Image, { uuid: stored.uuid }),
url: new URL(stored.publicUrl),
mediaType: file.type,
name: object.name,
});
},
)
.authorize(async (ctx, identifier) => {
const session = await verifyAccessToken(
ctx.request.headers.get("authorization"),
);
return session?.identifier === identifier;
});
The actor dispatcher advertises the endpoint itself by setting endpoints: new Endpoints({ uploadMedia: ctx.getMediaUploaderUri(identifier) }), the same way it places its inbox and outbox URIs. Without an authorize() hook, anyone who finds the endpoint can upload to it. For that reason, Fedify logs a warning when an uploader is registered without one, and when the returned object's URI does not match an object dispatcher or the actor does not advertise the endpoint. Four new @fedify/lint rules catch the same mistakes before they run. Fedify reads the upload into memory and sets no size limit of its own, so cap the request size at your reverse proxy. Storing the file and serving the object back are up to the application. See the Media upload chapter; the work is in #754 and #927.
Fedify decides several things about every inbox delivery: which signature mechanisms applied, which keys they tried, whether the actor owned the key, and what happened to the activity. Before this release, applications saw only part of that. A Linked Data Signature by a key the actor did not own was counted the same as a bad signature, and the key that verified a request was never handed to the application. The request in #1191 came from an effort to log these decisions for a debugging tool.
The new onRequestFinished() callback on inbox listeners receives an InboxRequestReport for every delivery to an inbox route, including rejected requests and requests that fail before verification:
federation
.setInboxListeners("/users/{identifier}/inbox", "/inbox")
.on(Follow, async (ctx, follow) => { /* ... */ })
.onRequestFinished(async (ctx, report) => {
await db.insertInboxLog({
inbox: report.inbox.kind, // "personal" | "shared" | "portable"
authentication: report.authentication.status,
outcome: report.outcome.type === "response"
? report.outcome.disposition // "processed" | "enqueued" | "duplicate" | ...
: `exception:${report.outcome.stage}`,
keyIds: report.attempts
.flatMap((attempt) => attempt.checks)
.flatMap((check) => check.triedKeys)
.map((key) => key.id?.href),
});
});
Each HTTP Signature, Linked Data Signature, or Object Integrity Proof check appears as an attempt with the actual keys it tried, including a stale cached key and its refreshed replacement. The report also carries the final authentication decision, such as actorKeyMismatch or proofPolicy, and the response or exception that ended the request. Fedify awaits the callback whether or not the trace is sampled. Errors it throws are logged without changing the response, and enqueued means the queue accepted the activity, not that a worker later processed it. Mock federations in @fedify/testing accept the same callback. See Observing inbox requests; the implementation is in #1201.
Actor dispatchers have been able to return a Tombstone since 2.2.0, but object dispatchers could only return null. A deleted post was answered with 404 Not Found, as if it had never existed, so applications that wanted 410 Gone, as ActivityPub recommends, needed their own route in front of Fedify.
Object dispatchers may now return a Tombstone, which Fedify serves with 410 Gone and the serialized tombstone after applying the authorization predicate:
federation.setObjectDispatcher(
Note,
"/users/{identifier}/notes/{id}",
async (ctx, values) => {
const post = await getPost(values.identifier, values.id);
if (post == null) return null;
if (post.deletedAt != null) {
return new Tombstone({
id: ctx.getObjectUri(Note, values),
formerType: Note,
deleted: post.deletedAt,
});
}
return new Note({ id: ctx.getObjectUri(Note, values), content: post.content });
},
);
The return type of ObjectDispatcher widens to TObject | Tombstone | null. RequestContext.getObject() still returns null for a tombstone unless you pass { tombstone: "passthrough" } or ask for the Tombstone or Object class itself. Dispatchers registered for Tombstone or Object that already returned tombstones now respond with 410 rather than 200. See Deleted objects; the change was proposed in #1112 and made in #1117.
Much of what Fedify fetches is chosen by the sender of an incoming request. Key IDs in signatures are one example, and the servers behind them can be slow on purpose. Several changes in 2.4.0 bound that work.
As #1130 points out, a request may carry several RFC 9421 signatures, and each one can make Fedify fetch a key from a URL of the sender's choosing. Fedify now verifies at most the first three, in Signature-Input order, and ignores the rest. A signature counts toward the limit even if it fails before its key is fetched, and a key named by several signatures is looked up once. The new FederationOptions.maxHttpSignatures option, added with the limit in #1166, changes it.
The built-in document loaders now give each call ten seconds by default, a limit proposed in #1131 and added in #1169. The time limit covers every redirect, alternate document, double-knocking retry, and the response body. A timed-out call throws a FetchError whose cause is a DOMException named "TimeoutError". Error responses are read in full before the loader throws, up to 1 MiB, so that a server cannot send a 404 header and then stall the body. FederationOptions.documentLoaderTimeout changes the limit or turns it off with null, and custom document loaders are not affected. Since #913, getDocumentLoader() also rejects HTML responses that do not advertise an ActivityPub alternate with a FetchError, rather than failing inside JSON.parse(), which #912 found cluttering error trackers.
Cached public keys and remembered HTTP Message Signatures specs used to live forever in the key–value store. #1017 reports a production Redis instance holding 194,424 public-key entries, about 165 MiB, for actors that had mostly stopped federating. Keys now expire after 30 days and specs after 90, configurable through FederationOptions.publicKeyTtl and FederationOptions.httpMessageSignaturesSpecTtl. Entries written by earlier versions have no expiry until they are next written. The Clearing legacy cache entries section shows how to remove them for Redis, PostgreSQL, and other stores. Heewon Chae (@heeoneie) implemented this in #1027, their first contribution to Fedify.
const federation = createFederation<void>({
kv,
maxHttpSignatures: 1, // default: 3
documentLoaderTimeout: { seconds: 5 }, // default: 10 seconds
publicKeyTtl: { days: 7 }, // default: 30 days
httpMessageSignaturesSpecTtl: { days: 30 }, // default: 90 days
});
Finally, verifyProof() now authenticates every option of an Ed25519 JCS proof except proofValue, including expires, domain, challenge, and nonce. It also rejects expired or malformed ones. Previously, a field such as expires could be added to an already signed proof without invalidating it. Jiwon Kwon (@z9mb1) spotted this in review of #968. Callers can now require a domain and challenge through VerifyProofOptions to prevent cross-domain or replayed use.
@fedify/adonisjs@fedify/adonisjs integrates Fedify with AdonisJS. Running node ace add @fedify/adonisjs registers a service provider that owns the federation's lifecycle and a server middleware that mounts it. It also scaffolds config/fedify.ts, start/federation.ts, and app/federation/main.ts. Controllers get a ctx.federation request context:
// app/federation/main.ts
import federation from "@fedify/adonisjs/services/builder";
import { Person } from "@fedify/vocab";
federation.setActorDispatcher("/actors/{identifier}", (ctx, identifier) =>
new Person({ id: ctx.getActorUri(identifier), preferredUsername: identifier }));
The middleware sits first in AdonisJS's server stack rather than its router stack. That way Fedify sees paths the router does not know, such as /.well-known/webfinger, and reads request bodies before the body parser consumes them. The package targets Node.js and is published to npm only. See the AdonisJS section of the integration guide.
Samuel Brinkmann (@sabrinkmann) contributed the package in #1006, closing #139, and built it on a prototype that Emelia Smith (@thisismissem) attached to that issue.
npm add @fedify/adonisjs
pnpm add @fedify/adonisjs
yarn add @fedify/adonisjs
@fedify/interaction-controlsGoToSocial interaction controls, FEP-044f quote authorization, and FEP-7aa9 featured collections share a request-and-authorization flow. A server asks to reply to, quote, or feature a post, and the author's server approves according to the post's interactionPolicy. @fedify/interaction-controls implements that flow for like, reply, announce, quote, and feature interactions. Each has a helper, such as likeInteraction or quoteInteraction, that creates and verifies requests and authorizations, evaluates policies, and recognizes interactions that arrived without a request:
import { likeInteraction } from "@fedify/interaction-controls";
// When a LikeRequest arrives:
const verified = await likeInteraction.verifyRequest(ctx, { request });
if (verified.verified) {
const decision = await likeInteraction.evaluatePolicy(ctx, {
subject: note,
requester: request.actorId!,
});
if (decision.result === "automatic") { // or "manual" or "denied"
const authorization = likeInteraction.createAuthorization({
id: new URL("https://example.com/authorizations/1"),
attributedTo: owner,
interactingObject: like,
interactionTarget: note,
});
// ...
}
}
The package does not install inbox listeners or store anything; the application decides where requests and authorizations live and when a human needs to approve. Verification fails closed. Without a policy, likes, replies, and announces are approved automatically, while quotes and features are denied. @fedify/vocab gains the FEP-7aa9 types this needs: FeaturedCollection, FeaturedItem, FeatureRequest, and FeatureAuthorization, along with actors' featuredCollections and InteractionPolicy.canFeature. Samuel Brinkmann asked for them in #810 once Mastodon 4.6 shipped the FEP. See the Interaction controls chapter; the work is in #914 and #929.
npm add @fedify/interaction-controls
pnpm add @fedify/interaction-controls
yarn add @fedify/interaction-controls
deno add jsr:@fedify/interaction-controls
bun add @fedify/interaction-controls
@fedify/netlify@fedify/netlify lets a Fedify application run on Netlify Functions without an external database or queue. NetlifyMessageQueue submits jobs as Netlify Async Workloads events, and createNetlifyQueueHandler() turns a Function into their consumer, with delayed delivery, native retries, and per-key FIFO ordering. NetlifyBlobsKvStore is a key–value store on Netlify Blobs with expiration, prefix listing, and compare-and-swap:
import {
createNetlifyQueueHandler,
NetlifyBlobsKvStore,
NetlifyMessageQueue,
} from "@fedify/netlify";
import { AsyncWorkloadsClient } from "@netlify/async-workloads";
import { getStore } from "@netlify/blobs";
export const kv = new NetlifyBlobsKvStore(
getStore({ name: "fedify", consistency: "strong" }),
);
export const queue = new NetlifyMessageQueue({
client: new AsyncWorkloadsClient(),
orderingKv: kv,
});
// netlify/functions/fedify-queue.ts
export default createNetlifyQueueHandler({
queue,
maxRetries: 4,
federation: () => builder.build({ kv, queue, manuallyStartQueue: true }),
});
The key–value store exists because of cost. #1010 describes a static blog whose ActivityPub traffic kept a Netlify Database from ever idling, which used most of the account's monthly credits. Jiwon Kwon contributed NetlifyBlobsKvStore in #1029, and the queue landed in #934. See Netlify Functions in the deployment guide.
npm add @fedify/netlify
pnpm add @fedify/netlify
yarn add @fedify/netlify
deno add jsr:@fedify/netlify
bun add @fedify/netlify
@fedify/pglite@fedify/pglite provides PgliteKvStore, a KvStore on an embedded PGlite database that you create and pass in. It uses the same schema as PostgresKvStore. It is meant for a single PGlite instance in a single runtime isolate, so it does not include a message queue: PGlite does not share data between processes.
import { PGlite } from "@electric-sql/pglite";
import { PgliteKvStore } from "@fedify/pglite";
const federation = createFederation({
kv: new PgliteKvStore(new PGlite("./data/fedify")),
});
ChanHaeng Lee contributed the package in #1020, together with testKvStore() in @fedify/testing. testKvStore() is a conformance suite for KvStore implementations, the counterpart to testMessageQueue(). See PgliteKvStore.
npm add @fedify/pglite
pnpm add @fedify/pglite
yarn add @fedify/pglite
deno add jsr:@fedify/pglite
bun add @fedify/pglite
Portable inbox forwarding, task deduplication, Netlify queue ordering, and the delivery circuit breaker all need an atomic compare-and-swap on the key–value store. PostgresKvStore now implements cas(), added in #934, and so does RedisKvStore, requested in #1163 and added in #1167. The Redis version uses a single-key Lua script, which works on standalone Redis and Redis Cluster. Before this, the circuit breaker fell back to racy updates on Redis.
Both come with conditions. RedisKvStore needs Redis to permit EVAL, and custom codecs must encode equal values identically. PostgresKvStore now creates logged tables by default, because losing sequence state in crash recovery could block queued events for good. It migrates existing unlogged tables when it initializes. That one-time migration rewrites the table and locks it exclusively, so schedule the upgrade accordingly if the table is large or busy; unlogged: true keeps the old behavior.
fedify init and the CLIfedify init can now scaffold a SvelteKit project, contributed by Jang Hanarae (@menele) in #971. Jang Hanarae also added a test task to generated projects in #990. It starts the app, waits for it to be ready, and checks that it resolves a local actor, which gives new projects a smoke test to run after scaffolding and after later changes. Before generating anything, fedify init now checks that the selected Deno, Bun, or Node.js meets Fedify's minimum version, or a framework's higher one, such as Astro's Node.js 22.12. It fails with a clear error in non-interactive mode, and in interactive mode it disables the affected package managers. Lee Jeongmin (@userjmmm) contributed the check in #981. Astro projects now use Astro 7, and since #936 @fedify/astro is tested against Astro 5, 6, and 7 with real builds on Node.js, Deno, and Bun, as #931 asked.
fedify tunnel gains fedify.com.es, a tunneling service run by the Fedify project that needs no account; the CLI pins its SSH host key and refuses a mismatched server before exposing a local port. localhost.run is removed in the same change, #940, because the service no longer exists. fedify lookup now times out each request after ten seconds when -T/--timeout is not given, and the option now also bounds the wait for a DNS lookup. The CLI moves to Optique 1.3.2 in #1197, which handles attached values like --timeout=30 consistently, gives better suggestions for mistyped options, and no longer hides errors for known options behind positional arguments before --.
@fedify/lint gains several rules beyond the media upload ones mentioned above. actor-preferred-username-required, requested in #895 and added in #1022, warns when an actor dispatcher returns an actor without preferredUsername. outbox-listener-delivery-not-awaited, from #1057 and #1067, reports an outbox listener that calls ctx.sendActivity() or ctx.forwardActivity() and drops the promise. On Cloudflare Workers, such an activity may never leave, because pending work is discarded once the response returns. await, return, Promise.all(), Promise.allSettled(), and waitUntil() count as handling the promise, while void, Promise.race(), and Promise.any() are accepted as a deliberate choice not to wait. The rule is a warning in the ESLint recommended configuration and an error in strict. Oxlint users enable it by name, and it is not available in Deno Lint.
In #1050, which resolves #900, the existing outbox-listener-delivery-required rule began following control flow instead of scanning the listener's source as text. It reports a listener whose only delivery calls sit in a dead branch, after an unconditional return or throw, or in a function that is never used, and it stays quiet when it cannot tell. Jae-Hyuk-Jang (@jhyuk) contributed this change and both new rules. archie (@ArchieTansaria) then taught both delivery rules about statically false loops and loop binding patterns in #1088. See the linting guide.
@fedify/vocab supports the FEP-22cd draft for translations. A Translation records a translated version's language, translators, source object, and source review time, and the new Object.translations property attaches them to a post without creating separate posts. See Translation metadata. (#1037, #1038)@fedify/vocab-tools gains an extraContext property option, which adds a JSON-LD context only when its terms are used, and a trustEmbeddedObjects type option. (#1037, #1038)suppressError: true handles are now logged as warnings rather than errors, so that they no longer show up as application errors. Contributed by Jae Hui Hong (@axz0612). (#933, #1035)enqueueMany() makes separate sends can set MessageQueue.atomicEnqueueMany to false. Fedify then rejects a multi-message batch governed by one deduplicationKey up front, since a partial send followed by a retry could enqueue duplicates. (#930, #934)Temporal bundle temporal-polyfill instead of requiring @js-temporal/polyfill at runtime, and type declarations rely on the standard esnext.temporal lib. (#823, #925)Accept header now includes the ActivityStreams profile on the JSON-LD media type, as ActivityPub and FEP-ef61 gateways require. (#834, #1077)fedify lookup --recurse reporting a timeout or other network failure as a possibly private object; it now reports the actual cause. (#1156, #1199)-p/--allow-private-address option of fedify webfinger being ignored. (#1156, #1199)outbox-listener-delivery-required and outbox-listener-delivery-not-awaited losing track of an object's functions when a nested helper assigns another property of that object. (#1125, #1140)npm add @fedify/fedify@2.4.0
pnpm add @fedify/fedify@2.4.0
yarn add @fedify/fedify@2.4.0
deno add jsr:@fedify/fedify@2.4.0
bun add @fedify/fedify@2.4.0
Update the other @fedify/* packages you use to 2.4.0 at the same time. To update the CLI, run npm install -g @fedify/cli@2.4.0, or see the installation instructions for other package managers.
Most applications need no code changes, but these changes in behavior may affect you:
401 Unauthorized unless they carry a valid Object Integrity Proof by the actor's DID. Keys at compatible identifiers, and keys at ordinary URLs that claim a portable owner, are no longer trusted by origin.Context as their options, such as await create.getObject(ctx), now verify the portable objects they dereference, including references to compatible identifiers from objects with ordinary HTTP(S) IDs./.well-known/apgateway/, and Context.sendActivity() may reject an activity that embeds others' portable objects. Next.js applications need the matcher shown above to serve hashlink media.maxHttpSignatures if you need more.fedify lookup does too.Tombstone or Object that return tombstones now respond with 410 Gone instead of 200 OK.PostgresKvStore migrates existing unlogged tables to logged tables on initialization, which locks the table.RedisKvStore needs EVAL permission for cas().Context and RequestContext interfaces need the new getPortable*Uri() methods and isSignedByAudience().fedify tunnel no longer supports localhost.run.Thanks to this release's contributors:
@fedify/adonisjs, and the request for FEP-7aa9 vocabularytest task in fedify initfedify initactor-preferred-username-required and outbox-listener-delivery-not-awaited lint rules, and control-flow analysis for outbox-listener-delivery-requiredChanHaeng Lee (@2chanhaeng) and Jiwon Kwon (@z9mb1) joined as maintainers after 2.3.0. ChanHaeng built the background task API and @fedify/pglite, Jiwon built NetlifyBlobsKvStore and rewrote the relay documentation, and both reviewed much of the rest of this release.
Many people added tests, documentation, and infrastructure during this cycle, often starting from a good first issue: Seoa Yoon (@bananamilk452), Lim Kyoujin (@dktsudgg), @m2nsp, Cho Seongmin (@seongmin36), Safal Shrestha (@Safal-Shrestha-SS), Jo Youngjae (@jaonz6057), @kecan0406, Loi Nguyen (@lntutor), Oh Taejun (@0w0), Yerin Park (@dstforye), Lee Dogeon (@moreal), and Sae Jin Kim (@siliconsjang).
The FEP-ef61 work owes a lot to people outside the project. @anotherdoesnm opened the request in #288. @silverpill, the author of FEP-ef61 and FEP-ae97, clarified the gateway trust model, reviewed the compound proof profile, and checked its test vectors. @dmitrizagidulin and @by_caballero weighed in on the profile's JSON-LD questions in #938. Testing against tootik and Mitra led to two of the interoperability fixes in this release.
See CHANGES.md for the complete changelog.
github.com
ActivityPub server framework in TypeScript. Contribute to fedify-dev/fedify development by creating an account on GitHub.
**NEBUVERSE – Mein eigenes Stück Fediverse**
Darin geht’s um meine eigene kleine GoToSocial-Instanz auf Worldnode:
Warum ich sie gebaut habe, wie ich GoToSocial ohne Docker auf Debian 13 eingerichtet habe, wie WebFinger, Relays und nginx zusammenspielen – und wie ich Speicherverbrauch, Backups und Restore-Tests gelöst habe.
Kein „Selfhosting ist Magie“-Gelaber, sondern mein echter Aufbau mit echten Messwerten und den Dingen, über die man beim Basteln wirklich stolpert.
🔗 https://www.dennishilk.com/de/blog/nebuverse-gotosocial-selbst-hosten/
#Fediverse #Mastodon #GoToSocial #SelfHosting #Linux #Debian #OpenSource #Homelab #ActivityPub #WebFinger

Collage-Grafik zum Blogartikel über NEBUVERSE. Groß im Bild steht „NEBUVERSE – Mein eigenes Stück Fediverse“. Daneben sind mehrere echte Screenshots der selbst gehosteten GoToSocial-Instanz zu sehen: das öffentliche Profil, die Einstellungen, die Speicher-Telemetrie und die Instanz-Startseite. Unten stehen die Hinweise „realer Aufbau“, „getestete Backups“ und „echte Metriken“. Das Design ist dunkelblau mit technischen UI-Elementen.
🆕 blog! “I paid people to try and follow my README”
Developers suck at writing documentation. We like to say RTFM but often there ain't no FM to R. At best we have a half-arsed collection of notes, some obsolete tips, and a link to a desolate forum full of people asking the same questions again and again. That's no way to treat our users.
I naïvely …
👀 Read more: https://shkspr.mobi/blog/2026/10/i-paid-people-to-try-and-follow-my-readme/
⸻
#ActivityBot #ActivityPub #developers
shkspr.mobi
Developers suck at writing documentation. We like to say RTFM but often there ain't no FM to R. At best we have a half-arsed collection of notes, some obsolete tips, and a link to a desolate forum full of people asking the same questions again and again. That's no way to treat our users. I naïvely believe there's a better way. As I've written before, you should sit down and actually test your …
**NEBUVERSE – Mein eigenes Stück Fediverse**
Darin geht’s um meine eigene kleine GoToSocial-Instanz auf Worldnode:
Warum ich sie gebaut habe, wie ich GoToSocial ohne Docker auf Debian 13 eingerichtet habe, wie WebFinger, Relays und nginx zusammenspielen – und wie ich Speicherverbrauch, Backups und Restore-Tests gelöst habe.
Kein „Selfhosting ist Magie“-Gelaber, sondern mein echter Aufbau mit echten Messwerten und den Dingen, über die man beim Basteln wirklich stolpert.
🔗 https://www.dennishilk.com/de/blog/nebuverse-gotosocial-selbst-hosten/
#Fediverse #Mastodon #GoToSocial #SelfHosting #Linux #Debian #OpenSource #Homelab #ActivityPub #WebFinger

Collage-Grafik zum Blogartikel über NEBUVERSE. Groß im Bild steht „NEBUVERSE – Mein eigenes Stück Fediverse“. Daneben sind mehrere echte Screenshots der selbst gehosteten GoToSocial-Instanz zu sehen: das öffentliche Profil, die Einstellungen, die Speicher-Telemetrie und die Instanz-Startseite. Unten stehen die Hinweise „realer Aufbau“, „getestete Backups“ und „echte Metriken“. Das Design ist dunkelblau mit technischen UI-Elementen.
🆕 blog! “I paid people to try and follow my README”
Developers suck at writing documentation. We like to say RTFM but often there ain't no FM to R. At best we have a half-arsed collection of notes, some obsolete tips, and a link to a desolate forum full of people asking the same questions again and again. That's no way to treat our users.
I naïvely …
👀 Read more: https://shkspr.mobi/blog/2026/10/i-paid-people-to-try-and-follow-my-readme/
⸻
#ActivityBot #ActivityPub #developers
shkspr.mobi
Developers suck at writing documentation. We like to say RTFM but often there ain't no FM to R. At best we have a half-arsed collection of notes, some obsolete tips, and a link to a desolate forum full of people asking the same questions again and again. That's no way to treat our users. I naïvely believe there's a better way. As I've written before, you should sit down and actually test your …
🆕 blog! “I paid people to try and follow my README”
Developers suck at writing documentation. We like to say RTFM but often there ain't no FM to R. At best we have a half-arsed collection of notes, some obsolete tips, and a link to a desolate forum full of people asking the same questions again and again. That's no way to treat our users.
I naïvely …
👀 Read more: https://shkspr.mobi/blog/2026/10/i-paid-people-to-try-and-follow-my-readme/
⸻
#ActivityBot #ActivityPub #developers
shkspr.mobi
Developers suck at writing documentation. We like to say RTFM but often there ain't no FM to R. At best we have a half-arsed collection of notes, some obsolete tips, and a link to a desolate forum full of people asking the same questions again and again. That's no way to treat our users. I naïvely believe there's a better way. As I've written before, you should sit down and actually test your …
I think I would love to have a ActivityPub based Pinterest??!
Like being able to see things from specific networks.. No slop instances..
In the end it is kind of already there with pixelfed, but it comes down to a how it is presented.
I think I would love to have a ActivityPub based Pinterest??!
Like being able to see things from specific networks.. No slop instances..
In the end it is kind of already there with pixelfed, but it comes down to a how it is presented.
We are preparing a FEP for our Ephemeral Stories feature!
It's more than just a special followers only post that expires after 24 hours, we also support rich text overlays, hashtags, links and mentions that require proper implementation.
We've supported Stories for years, but only delivered to known Pixelfed servers, but that is going to change once we finish the v2 rollout!
Stay tuned.
🆕 blog! “I paid people to try and follow my README”
Developers suck at writing documentation. We like to say RTFM but often there ain't no FM to R. At best we have a half-arsed collection of notes, some obsolete tips, and a link to a desolate forum full of people asking the same questions again and again. That's no way to treat our users.
I naïvely …
👀 Read more: https://shkspr.mobi/blog/2026/10/i-paid-people-to-try-and-follow-my-readme/
⸻
#ActivityBot #ActivityPub #developers
shkspr.mobi
Developers suck at writing documentation. We like to say RTFM but often there ain't no FM to R. At best we have a half-arsed collection of notes, some obsolete tips, and a link to a desolate forum full of people asking the same questions again and again. That's no way to treat our users. I naïvely believe there's a better way. As I've written before, you should sit down and actually test your …
We are preparing a FEP for our Ephemeral Stories feature!
It's more than just a special followers only post that expires after 24 hours, we also support rich text overlays, hashtags, links and mentions that require proper implementation.
We've supported Stories for years, but only delivered to known Pixelfed servers, but that is going to change once we finish the v2 rollout!
Stay tuned.
We are preparing a FEP for our Ephemeral Stories feature!
It's more than just a special followers only post that expires after 24 hours, we also support rich text overlays, hashtags, links and mentions that require proper implementation.
We've supported Stories for years, but only delivered to known Pixelfed servers, but that is going to change once we finish the v2 rollout!
Stay tuned.
I still don't think I understand why it was felt necessary to invent #ActivityPub, when #NNTP already existed and is proven to scale.
Why couldn't we just have an updated HTTP to NNTP gateway front end? #Fediverse
I still don't think I understand why it was felt necessary to invent #ActivityPub, when #NNTP already existed and is proven to scale.
Why couldn't we just have an updated HTTP to NNTP gateway front end? #Fediverse
Derzeit läuft die gängige Kommunikation im Fediverse ja über eine Server 2 Server (S2S) API, also die verschiedenen Installationen der Plattformen kommunizieren/federieren miteinander und die Benutzer nutzen die Weboberfläche oder eine App die über spezifische Schnittstellen mit den Plattformen kommuniziert. Eine Umsetzung von C2S ist mir jedoch nicht bekannt, wo das doch wirklich schlanke Fediverse Server die nicht den Ballast eines Frontends mit sich schleppen müssten erlauben würde. Sprich es würde zu einer noch sehr viel weitergehenden Dezentralisierung kommen. Natürlich könnte man wenn man das will auch weiterhin einen Fediverse Server und ein Webfrontend auf einen Server packen.
#Fediverse #ActivityPub #C2S #Client-to-Server #Schnittstelle #API #Frage #Kommunikation #Dezentralität @dach
🆕 blog! “Some quick thoughts on Unit Testing ActivityPub”
I'll be radically honest here - I never really got the idea of "Test Driven Development". I'm much more of a "telnet into production and damn the consequences" kind of guy. Look, it's much more fun and my cardiologist recommends severe shocks every now and again.
The essence of unit testing is pretty…
👀 Read more: https://shkspr.mobi/blog/2026/10/some-thoughts-on-unit-testing-activitypub/
⸻
#ActivityBot #ActivityPub #php
shkspr.mobi
I'll be radically honest here - I never really got the idea of "Test Driven Development". I'm much more of a "telnet into production and damn the consequences" kind of guy. Look, it's much more fun and my cardiologist recommends severe shocks every now and again. The essence of unit testing is pretty simple: Feed a function some data See how it responds Send it good data and if it returns…
Derzeit läuft die gängige Kommunikation im Fediverse ja über eine Server 2 Server (S2S) API, also die verschiedenen Installationen der Plattformen kommunizieren/federieren miteinander und die Benutzer nutzen die Weboberfläche oder eine App die über spezifische Schnittstellen mit den Plattformen kommuniziert. Eine Umsetzung von C2S ist mir jedoch nicht bekannt, wo das doch wirklich schlanke Fediverse Server die nicht den Ballast eines Frontends mit sich schleppen müssten erlauben würde. Sprich es würde zu einer noch sehr viel weitergehenden Dezentralisierung kommen. Natürlich könnte man wenn man das will auch weiterhin einen Fediverse Server und ein Webfrontend auf einen Server packen.
#Fediverse #ActivityPub #C2S #Client-to-Server #Schnittstelle #API #Frage #Kommunikation #Dezentralität @dach
And here's the episode of Search Engine where they talk about why the Forkiverse is ending — rising costs, lack of exciting content, and Russian propaganda infiltration.
searchengine.show
Nine months ago, three podcasters attempted to build a better internet. Today -- they confront foreign adversaries on a digital battlefield.
🆕 blog! “Some quick thoughts on Unit Testing ActivityPub”
I'll be radically honest here - I never really got the idea of "Test Driven Development". I'm much more of a "telnet into production and damn the consequences" kind of guy. Look, it's much more fun and my cardiologist recommends severe shocks every now and again.
The essence of unit testing is pretty…
👀 Read more: https://shkspr.mobi/blog/2026/10/some-thoughts-on-unit-testing-activitypub/
⸻
#ActivityBot #ActivityPub #php
shkspr.mobi
I'll be radically honest here - I never really got the idea of "Test Driven Development". I'm much more of a "telnet into production and damn the consequences" kind of guy. Look, it's much more fun and my cardiologist recommends severe shocks every now and again. The essence of unit testing is pretty simple: Feed a function some data See how it responds Send it good data and if it returns…
Servers
- Hollo v0.9.22
- PieFed v1.8.0
- Mitra v5.11.0
- Mbin v1.11.0
- Catodon v26.10.0
- NeoDB v0.19.6.3
- NodeBB v4.16.2
- Lemmy Development Update September 2026
Clients
- Tusky v33.0
- Pachli v3.9.1
- IceCubesApp v2.1.4
- Summit v1.87.0
- Pixelfed Mobile App v2.0.0
- Aria v1.5.15
For developers
- Fedify security updates: 2.0.30, 2.1.26, 2.2.15, 2.3.10, and 2.4.0
Protocol
- FEP-4f18: ActivityPub Network Time
-----
#WeekInFediverse #Fediverse #ActivityPub
Previous edition: https://mitra.social/objects/01a0fe1a-a0fe-7d20-895a-b18c4561484c
mitra.social
Week in Fediverse 2026-10-02 - Post by @weekinfediverse
Servers
- Hollo v0.9.22
- PieFed v1.8.0
- Mitra v5.11.0
- Mbin v1.11.0
- Catodon v26.10.0
- NeoDB v0.19.6.3
- NodeBB v4.16.2
- Lemmy Development Update September 2026
Clients
- Tusky v33.0
- Pachli v3.9.1
- IceCubesApp v2.1.4
- Summit v1.87.0
- Pixelfed Mobile App v2.0.0
- Aria v1.5.15
For developers
- Fedify security updates: 2.0.30, 2.1.26, 2.2.15, 2.3.10, and 2.4.0
Protocol
- FEP-4f18: ActivityPub Network Time
-----
#WeekInFediverse #Fediverse #ActivityPub
Previous edition: https://mitra.social/objects/01a0fe1a-a0fe-7d20-895a-b18c4561484c
mitra.social
Week in Fediverse 2026-10-02 - Post by @weekinfediverse
To help with their Forkiverse experiment, the Search Engine/Hard Fork teams talked to two experts: @evan and @jaz. Listen to @mike's chats with them here:
https://flipboard.video/w/nLKq9MFfyMdLaqAmHtDUWj
flipboard.video
The fediverse offers an opportunity to rethink how trust and safety works in social media. In a decentralized environment, creating safe and welcoming places relies on community moderation, transpa...
Servers
- Hollo v0.9.22
- PieFed v1.8.0
- Mitra v5.11.0
- Mbin v1.11.0
- Catodon v26.10.0
- NeoDB v0.19.6.3
- NodeBB v4.16.2
- Lemmy Development Update September 2026
Clients
- Tusky v33.0
- Pachli v3.9.1
- IceCubesApp v2.1.4
- Summit v1.87.0
- Pixelfed Mobile App v2.0.0
- Aria v1.5.15
For developers
- Fedify security updates: 2.0.30, 2.1.26, 2.2.15, 2.3.10, and 2.4.0
Protocol
- FEP-4f18: ActivityPub Network Time
-----
#WeekInFediverse #Fediverse #ActivityPub
Previous edition: https://mitra.social/objects/01a0fe1a-a0fe-7d20-895a-b18c4561484c
mitra.social
Week in Fediverse 2026-10-02 - Post by @weekinfediverse
Servers
- Hollo v0.9.22
- PieFed v1.8.0
- Mitra v5.11.0
- Mbin v1.11.0
- Catodon v26.10.0
- NeoDB v0.19.6.3
- NodeBB v4.16.2
- Lemmy Development Update September 2026
Clients
- Tusky v33.0
- Pachli v3.9.1
- IceCubesApp v2.1.4
- Summit v1.87.0
- Pixelfed Mobile App v2.0.0
- Aria v1.5.15
For developers
- Fedify security updates: 2.0.30, 2.1.26, 2.2.15, 2.3.10, and 2.4.0
Protocol
- FEP-4f18: ActivityPub Network Time
-----
#WeekInFediverse #Fediverse #ActivityPub
Previous edition: https://mitra.social/objects/01a0fe1a-a0fe-7d20-895a-b18c4561484c
mitra.social
Week in Fediverse 2026-10-02 - Post by @weekinfediverse
To help with their Forkiverse experiment, the Search Engine/Hard Fork teams talked to two experts: @evan and @jaz. Listen to @mike's chats with them here:
https://flipboard.video/w/nLKq9MFfyMdLaqAmHtDUWj
flipboard.video
The fediverse offers an opportunity to rethink how trust and safety works in social media. In a decentralized environment, creating safe and welcoming places relies on community moderation, transpa...
And here's the episode of Search Engine where they talk about why the Forkiverse is ending — rising costs, lack of exciting content, and Russian propaganda infiltration.
searchengine.show
Nine months ago, three podcasters attempted to build a better internet. Today -- they confront foreign adversaries on a digital battlefield.
At the beginning of this year, @kevin, @caseynewton and @pj launched a fediverse instance, The Forkiverse. Now, they're shutting it down. Here's PJ's conversation with @mike for #DotSocial about the experiment.
flipboard.video
Dot Social’s Mike McCue talks to PJ Vogt, host of the Search Engine podcast. PJ is also a co-founder of the Forkiverse, a Mastodon instance launched with the Hard Fork team as an experiment in comm...
From our series on the public funding of open source, Jeffrey A. McGuire explains how the Fediverse was built.
@nlnet @NGIZero @cwebber @spritely @Bonfire
#DigitalAutonomy #ActivityPub #Fediverse #OpenSource #community #FOSS
Linux Magazine talked to us about how and why we build Bonfire.
What we want next is "not a bigger Bonfire, but a network of cooperatives covering the whole supply chain of a community's digital space", who "do it together" rather than leave you to "do it yourself".
Because "federation, we don't see it as just a technical thing. We see it as a social, economic, and political tool as well."
Link: Supporting the Social Web: How the Fediverse Found Parts for Its Platforms
Also happy to spread the love for @coopcloud@social.coop, @coquest@social.coop, @SocialCoop@social.coop and more, check out the links at the end of the article! Thanks to @horncologne@fosstodon.org for a lovely conversation, and @linuxmagazine@fosstodon.org for publishing it, in a piece that also features the great @cwebber@social.coop and @tsyesika@mastodon.nu from @spritely@social.coop.
#fediverse #ActivityPub #opensource #freesoftware #OpenCooperativeEcosystem #OpenScienceNetwork
bonfire.cafe
A space for Bonfire maintainers and contributors to communicate
From our series on the public funding of open source, Jeffrey A. McGuire explains how the Fediverse was built.
@nlnet @NGIZero @cwebber @spritely @Bonfire
#DigitalAutonomy #ActivityPub #Fediverse #OpenSource #community #FOSS
From our series on the public funding of open source, Jeffrey A. McGuire explains how the Fediverse was built.
@nlnet @NGIZero @cwebber @spritely @Bonfire
#DigitalAutonomy #ActivityPub #Fediverse #OpenSource #community #FOSS
From our series on the public funding of open source, Jeffrey A. McGuire explains how the Fediverse was built.
@nlnet @NGIZero @cwebber @spritely @Bonfire
#DigitalAutonomy #ActivityPub #Fediverse #OpenSource #community #FOSS
@evanprodromou @jdlh OK, I also got the activitypub.bot codebase (tags.pub and groups.pub too) to support it. The new version is 0.53.1, and the relevant ticket is here:
https://github.com/evanp/activitypub-bot/issues/282
I'm going to go after onepage.pub next; it's a completely different codebase.
github.com
We need to confirm the following functionality: Receive: Can local users receive activities from a remote account with a non-ASCII handle? Send: Can local users send activities to a remote account ...
OK, who are the experts on #DistributedSystems on the #Fediverse? There is a new #FEP for implementing a scalar #LogicalClock in #ActivityPub, #FEP4f18.
I'm especially interested in mitigations for Byzantine faults in the system; right now, it's kind of hand-wavy based on value and trust heuristics:
helge.codeberg.page
Logical clocks capture the causal ordering of events in a system. This proposal introduces a logical clock system for the ActivityPub network.
@evanprodromou @jdlh I'm glad to say that the `webfinger` and `activitypub-webfinger` NodeJS libraries now support globally inclusive handles.
https://www.npmjs.com/package/webfinger
https://www.npmjs.com/package/activitypub-webfinger
Linux Magazine talked to us about how and why we build Bonfire.
What we want next is "not a bigger Bonfire, but a network of cooperatives covering the whole supply chain of a community's digital space", who "do it together" rather than leave you to "do it yourself".
Because "federation, we don't see it as just a technical thing. We see it as a social, economic, and political tool as well."
Link: Supporting the Social Web: How the Fediverse Found Parts for Its Platforms
Also happy to spread the love for @coopcloud@social.coop, @coquest@social.coop, @SocialCoop@social.coop and more, check out the links at the end of the article! Thanks to @horncologne@fosstodon.org for a lovely conversation, and @linuxmagazine@fosstodon.org for publishing it, in a piece that also features the great @cwebber@social.coop and @tsyesika@mastodon.nu from @spritely@social.coop.
#fediverse #ActivityPub #opensource #freesoftware #OpenCooperativeEcosystem #OpenScienceNetwork
bonfire.cafe
A space for Bonfire maintainers and contributors to communicate
Linux Magazine talked to us about how and why we build Bonfire.
What we want next is "not a bigger Bonfire, but a network of cooperatives covering the whole supply chain of a community's digital space", who "do it together" rather than leave you to "do it yourself".
Because "federation, we don't see it as just a technical thing. We see it as a social, economic, and political tool as well."
Link: Supporting the Social Web: How the Fediverse Found Parts for Its Platforms
Also happy to spread the love for @coopcloud@social.coop, @coquest@social.coop, @SocialCoop@social.coop and more, check out the links at the end of the article! Thanks to @horncologne@fosstodon.org for a lovely conversation, and @linuxmagazine@fosstodon.org for publishing it, in a piece that also features the great @cwebber@social.coop and @tsyesika@mastodon.nu from @spritely@social.coop.
#fediverse #ActivityPub #opensource #freesoftware #OpenCooperativeEcosystem #OpenScienceNetwork
bonfire.cafe
A space for Bonfire maintainers and contributors to communicate
Linux Magazine talked to us about how and why we build Bonfire.
What we want next is "not a bigger Bonfire, but a network of cooperatives covering the whole supply chain of a community's digital space", who "do it together" rather than leave you to "do it yourself".
Because "federation, we don't see it as just a technical thing. We see it as a social, economic, and political tool as well."
Link: Supporting the Social Web: How the Fediverse Found Parts for Its Platforms
Also happy to spread the love for @coopcloud@social.coop, @coquest@social.coop, @SocialCoop@social.coop and more, check out the links at the end of the article! Thanks to @horncologne@fosstodon.org for a lovely conversation, and @linuxmagazine@fosstodon.org for publishing it, in a piece that also features the great @cwebber@social.coop and @tsyesika@mastodon.nu from @spritely@social.coop.
#fediverse #ActivityPub #opensource #freesoftware #OpenCooperativeEcosystem #OpenScienceNetwork
bonfire.cafe
A space for Bonfire maintainers and contributors to communicate
Linux Magazine talked to us about how and why we build Bonfire.
What we want next is "not a bigger Bonfire, but a network of cooperatives covering the whole supply chain of a community's digital space", who "do it together" rather than leave you to "do it yourself".
Because "federation, we don't see it as just a technical thing. We see it as a social, economic, and political tool as well."
Link: Supporting the Social Web: How the Fediverse Found Parts for Its Platforms
Also happy to spread the love for @coopcloud@social.coop, @coquest@social.coop, @SocialCoop@social.coop and more, check out the links at the end of the article! Thanks to @horncologne@fosstodon.org for a lovely conversation, and @linuxmagazine@fosstodon.org for publishing it, in a piece that also features the great @cwebber@social.coop and @tsyesika@mastodon.nu from @spritely@social.coop.
#fediverse #ActivityPub #opensource #freesoftware #OpenCooperativeEcosystem #OpenScienceNetwork
bonfire.cafe
A space for Bonfire maintainers and contributors to communicate
Linux Magazine talked to us about how and why we build Bonfire.
What we want next is "not a bigger Bonfire, but a network of cooperatives covering the whole supply chain of a community's digital space", who "do it together" rather than leave you to "do it yourself".
Because "federation, we don't see it as just a technical thing. We see it as a social, economic, and political tool as well."
Link: Supporting the Social Web: How the Fediverse Found Parts for Its Platforms
Also happy to spread the love for @coopcloud@social.coop, @coquest@social.coop, @SocialCoop@social.coop and more, check out the links at the end of the article! Thanks to @horncologne@fosstodon.org for a lovely conversation, and @linuxmagazine@fosstodon.org for publishing it, in a piece that also features the great @cwebber@social.coop and @tsyesika@mastodon.nu from @spritely@social.coop.
#fediverse #ActivityPub #opensource #freesoftware #OpenCooperativeEcosystem #OpenScienceNetwork
bonfire.cafe
A space for Bonfire maintainers and contributors to communicate
Linux Magazine talked to us about how and why we build Bonfire.
What we want next is "not a bigger Bonfire, but a network of cooperatives covering the whole supply chain of a community's digital space", who "do it together" rather than leave you to "do it yourself".
Because "federation, we don't see it as just a technical thing. We see it as a social, economic, and political tool as well."
Link: Supporting the Social Web: How the Fediverse Found Parts for Its Platforms
Also happy to spread the love for @coopcloud@social.coop, @coquest@social.coop, @SocialCoop@social.coop and more, check out the links at the end of the article! Thanks to @horncologne@fosstodon.org for a lovely conversation, and @linuxmagazine@fosstodon.org for publishing it, in a piece that also features the great @cwebber@social.coop and @tsyesika@mastodon.nu from @spritely@social.coop.
#fediverse #ActivityPub #opensource #freesoftware #OpenCooperativeEcosystem #OpenScienceNetwork
bonfire.cafe
A space for Bonfire maintainers and contributors to communicate
Linux Magazine talked to us about how and why we build Bonfire.
What we want next is "not a bigger Bonfire, but a network of cooperatives covering the whole supply chain of a community's digital space", who "do it together" rather than leave you to "do it yourself".
Because "federation, we don't see it as just a technical thing. We see it as a social, economic, and political tool as well."
Link: Supporting the Social Web: How the Fediverse Found Parts for Its Platforms
Also happy to spread the love for @coopcloud@social.coop, @coquest@social.coop, @SocialCoop@social.coop and more, check out the links at the end of the article! Thanks to @horncologne@fosstodon.org for a lovely conversation, and @linuxmagazine@fosstodon.org for publishing it, in a piece that also features the great @cwebber@social.coop and @tsyesika@mastodon.nu from @spritely@social.coop.
#fediverse #ActivityPub #opensource #freesoftware #OpenCooperativeEcosystem #OpenScienceNetwork
bonfire.cafe
A space for Bonfire maintainers and contributors to communicate
Linux Magazine talked to us about how and why we build Bonfire.
What we want next is "not a bigger Bonfire, but a network of cooperatives covering the whole supply chain of a community's digital space", who "do it together" rather than leave you to "do it yourself".
Because "federation, we don't see it as just a technical thing. We see it as a social, economic, and political tool as well."
Link: Supporting the Social Web: How the Fediverse Found Parts for Its Platforms
Also happy to spread the love for @coopcloud@social.coop, @coquest@social.coop, @SocialCoop@social.coop and more, check out the links at the end of the article! Thanks to @horncologne@fosstodon.org for a lovely conversation, and @linuxmagazine@fosstodon.org for publishing it, in a piece that also features the great @cwebber@social.coop and @tsyesika@mastodon.nu from @spritely@social.coop.
#fediverse #ActivityPub #opensource #freesoftware #OpenCooperativeEcosystem #OpenScienceNetwork
bonfire.cafe
A space for Bonfire maintainers and contributors to communicate
Linux Magazine talked to us about how and why we build Bonfire.
What we want next is "not a bigger Bonfire, but a network of cooperatives covering the whole supply chain of a community's digital space", who "do it together" rather than leave you to "do it yourself".
Because "federation, we don't see it as just a technical thing. We see it as a social, economic, and political tool as well."
Link: Supporting the Social Web: How the Fediverse Found Parts for Its Platforms
Also happy to spread the love for @coopcloud@social.coop, @coquest@social.coop, @SocialCoop@social.coop and more, check out the links at the end of the article! Thanks to @horncologne@fosstodon.org for a lovely conversation, and @linuxmagazine@fosstodon.org for publishing it, in a piece that also features the great @cwebber@social.coop and @tsyesika@mastodon.nu from @spritely@social.coop.
#fediverse #ActivityPub #opensource #freesoftware #OpenCooperativeEcosystem #OpenScienceNetwork
bonfire.cafe
A space for Bonfire maintainers and contributors to communicate
Tijd voor taart en slingers. We hebben zojuist succesvol het account van @NRC gemigreerd van mastodon.nl naar onze eigen server nrc.social. Tegen de vrijwilligers achter mastodon.nl zeg ik: dank voor de goede zorgen! I'll take it from here. Overigens blijft het devies dat we klein beginnen. Ik zal verslag doen van de ervaringen. https://www.nrc.nl/nieuws/2026/10/08/zojuist-is-het-account-van-nrc-naar-de-eigen-server-verhuisd-a4938498
#mastodon #opensocials #NRCserver #taart #activitypub
nrc.nl
Een kleine, maar leuke en betekenisvolle stap: het NRC-account op Mastodon is zojuist naar onze eigen server verhuisd. De 12.000 volgers van NRC op dit sociale netwerk verhuizen automatisch mee. Ze hoeven niets te doen. De server is hier te vinden. Bovenstaande zinnen behoeven wat uitleg: Mastodon is software die wordt gebruikt om een alternatief socialemedianetwerk te bouwen. Dat netwerk bestaat…
Tijd voor taart en slingers. We hebben zojuist succesvol het account van @NRC gemigreerd van mastodon.nl naar onze eigen server nrc.social. Tegen de vrijwilligers achter mastodon.nl zeg ik: dank voor de goede zorgen! I'll take it from here. Overigens blijft het devies dat we klein beginnen. Ik zal verslag doen van de ervaringen. https://www.nrc.nl/nieuws/2026/10/08/zojuist-is-het-account-van-nrc-naar-de-eigen-server-verhuisd-a4938498
#mastodon #opensocials #NRCserver #taart #activitypub
nrc.nl
Een kleine, maar leuke en betekenisvolle stap: het NRC-account op Mastodon is zojuist naar onze eigen server verhuisd. De 12.000 volgers van NRC op dit sociale netwerk verhuizen automatisch mee. Ze hoeven niets te doen. De server is hier te vinden. Bovenstaande zinnen behoeven wat uitleg: Mastodon is software die wordt gebruikt om een alternatief socialemedianetwerk te bouwen. Dat netwerk bestaat…
Dear #Admins & #Developers of the #Fediverse,
ICYMI: #AgeVerification (and more) vs. the #Fediverse: How the EU #KidsAct challenges #Mastodon (& Co.)
must-read by @wintermeyer
→ https://www.heise.de/en/background/Age-verification-in-the-Fediverse-How-the-KIDS-Act-challenges-Mastodon-11474306.html
The EU Commission seeks feedback until ⏰️ 26 Nov. 2026:
→ https://digital-strategy.ec.europa.eu/en/news/commission-seeks-feedback-eu-kids-act
cc @fediforum @ @teamtoot @admin @itteam @dansup @fed.brid.gy @fediway
#Fediforum #Fediadmin #Fedikirche #FediLZ #Fediday #OpenSocialWeb #Loops #Pixelfed #ActivityPub #CivicTech
digital-strategy.ec.europa.eu
The Commission is gathering feedback on the proposed EU Kids Act.
Dear #Admins & #Developers of the #Fediverse,
ICYMI: #AgeVerification (and more) vs. the #Fediverse: How the EU #KidsAct challenges #Mastodon (& Co.)
must-read by @wintermeyer
→ https://www.heise.de/en/background/Age-verification-in-the-Fediverse-How-the-KIDS-Act-challenges-Mastodon-11474306.html
The EU Commission seeks feedback until ⏰️ 26 Nov. 2026:
→ https://digital-strategy.ec.europa.eu/en/news/commission-seeks-feedback-eu-kids-act
cc @fediforum @ @teamtoot @admin @itteam @dansup @fed.brid.gy @fediway
#Fediforum #Fediadmin #Fedikirche #FediLZ #Fediday #OpenSocialWeb #Loops #Pixelfed #ActivityPub #CivicTech
digital-strategy.ec.europa.eu
The Commission is gathering feedback on the proposed EU Kids Act.
Dear #Admins & #Developers of the #Fediverse,
ICYMI: #AgeVerification (and more) vs. the #Fediverse: How the EU #KidsAct challenges #Mastodon (& Co.)
must-read by @wintermeyer
→ https://www.heise.de/en/background/Age-verification-in-the-Fediverse-How-the-KIDS-Act-challenges-Mastodon-11474306.html
The EU Commission seeks feedback until ⏰️ 26 Nov. 2026:
→ https://digital-strategy.ec.europa.eu/en/news/commission-seeks-feedback-eu-kids-act
cc @fediforum @ @teamtoot @admin @itteam @dansup @fed.brid.gy @fediway
#Fediforum #Fediadmin #Fedikirche #FediLZ #Fediday #OpenSocialWeb #Loops #Pixelfed #ActivityPub #CivicTech
digital-strategy.ec.europa.eu
The Commission is gathering feedback on the proposed EU Kids Act.
Very excited to officially kickstart our @Bonfire@bonfire.cafe server at Warwick and to be able to codesign it with fellow researchers!
bonfire.cim.warwick.ac.uk
**What would online spaces look like if the people using them designed them? And what can we learn by researching it together as we go?** Join us on Wednesday 14 October, 3 to 5 pm, in REX 1 & 2
Very excited to officially kickstart our @Bonfire@bonfire.cafe server at Warwick and to be able to codesign it with fellow researchers!
bonfire.cim.warwick.ac.uk
**What would online spaces look like if the people using them designed them? And what can we learn by researching it together as we go?** Join us on Wednesday 14 October, 3 to 5 pm, in REX 1 & 2
Very excited to officially kickstart our @Bonfire@bonfire.cafe server at Warwick and to be able to codesign it with fellow researchers!
bonfire.cim.warwick.ac.uk
**What would online spaces look like if the people using them designed them? And what can we learn by researching it together as we go?** Join us on Wednesday 14 October, 3 to 5 pm, in REX 1 & 2
@laurenshof is opening the "decentralized technologies" track at @oscl
Big thanks to @_elena and @Gina for hosting our track as well as to @paolo and the whole team for organising #oscl26
#opensocialweb #fediverse #socialnetworks #activitypub #opensource #opencommunications #decentralized #digitalcommons #solidarity
@laurenshof is opening the "decentralized technologies" track at @oscl
Big thanks to @_elena and @Gina for hosting our track as well as to @paolo and the whole team for organising #oscl26
#opensocialweb #fediverse #socialnetworks #activitypub #opensource #opencommunications #decentralized #digitalcommons #solidarity
@laurenshof is opening the "decentralized technologies" track at @oscl
Big thanks to @_elena and @Gina for hosting our track as well as to @paolo and the whole team for organising #oscl26
#opensocialweb #fediverse #socialnetworks #activitypub #opensource #opencommunications #decentralized #digitalcommons #solidarity
@laurenshof is opening the "decentralized technologies" track at @oscl
Big thanks to @_elena and @Gina for hosting our track as well as to @paolo and the whole team for organising #oscl26
#opensocialweb #fediverse #socialnetworks #activitypub #opensource #opencommunications #decentralized #digitalcommons #solidarity
@laurenshof is opening the "decentralized technologies" track at @oscl
Big thanks to @_elena and @Gina for hosting our track as well as to @paolo and the whole team for organising #oscl26
#opensocialweb #fediverse #socialnetworks #activitypub #opensource #opencommunications #decentralized #digitalcommons #solidarity
@laurenshof displays nice vizualisation about his understanding of the #opensocialweb ecosystem which includes #Activitypub #ATproto and #Matrix
@laurenshof is opening the "decentralized technologies" track at @oscl
Big thanks to @_elena and @Gina for hosting our track as well as to @paolo and the whole team for organising #oscl26
#opensocialweb #fediverse #socialnetworks #activitypub #opensource #opencommunications #decentralized #digitalcommons #solidarity
@laurenshof is opening the "decentralized technologies" track at @oscl
Big thanks to @_elena and @Gina for hosting our track as well as to @paolo and the whole team for organising #oscl26
#opensocialweb #fediverse #socialnetworks #activitypub #opensource #opencommunications #decentralized #digitalcommons #solidarity
@evan @evanprodromou @jdlh @Edent
PS. On the proliferation of SWICG task forces, just musing out loud..
The #ActivityPub Webfinger TF implies a flavor of the #Webfinger open standard. Webfinger is only a small 'moving part' within the AP protocol intrinsics.
A task force focusing more broadly on the cross-cutting concern of "Internationalization and Localization" of the entire protocol suite, might've been a better way to slice related activities.
Activity Relay 3.1.0 and Activity Relay Directory 1.3.0 are released!
Activity Relay 3.1.0 and Activity Relay Directory 1.3.0 are released. This synchronized release brings together some notable improvements in the heartbeat protocol. These enhancements allow a Relay operator to push stats and information to Directory servers automatically, keeping information up to date.
Live installations of the Relay and Directory may be found at:
https://directory.argentwolf.org
On the Relay itself, the policy information will be presented on the default home page:

On the Directory, this information will be reflected as well with time stamps on when it was received and last checked for health by the Directory:

Other relays may be manually added to the Directory by the Directory operator/Admin. I have manually collected a list of Fediverse relays and populated my Directory installation with information about them. These sites are not automatically updated with regard to policy, but are verified as reachable by the Directory server.

Relays that are restricted (Approval Required) or closed to sign ups will appear as such on the Directory. Relays that go offline or are otherwise unreachable will appear in the offline list, eventually moving to the Graveyard after 30 days of offline time. Offline relays can be added the the Directory, but they will not become visible on the index until they pass an online check.

As always, if you want to make your own front facing website, you can disable the built in pages and use your own endpoints!
Comments, feedback, and bug reports are all welcome! Repo links below, available in both .deb and docker images. If you found these useful, please consider a tip to my Ko-fi to support further development! Thank you!
https://github.com/thystra/Activity-Relay
register, heartbeat, and sync commands so they coordinate cleanly with the background scheduler.--test-config validation with safer handling of optional profile errors.SUPPORT information to the generated relay website, including links and plain-text values such as cryptocurrency addresses.linux/amd64 and linux/arm64 at ghcr.io/thystra/activity-relay:3.1.0.https://github.com/thystra/activity-relay-directory
override > relay > csv./v2/relays and the public Directory with profile information, site counts, and Open/Restricted/Closed registration status./v1/relays compatibility API.linux/amd64 at ghcr.io/thystra/activity-relay-directory:1.3.0.Activity Relay 3.1.0 and Activity Relay Directory 1.3.0 are released!
Activity Relay 3.1.0 and Activity Relay Directory 1.3.0 are released. This synchronized release brings together some notable improvements in the heartbeat protocol. These enhancements allow a Relay operator to push stats and information to Directory servers automatically, keeping information up to date.
Live installations of the Relay and Directory may be found at:
https://directory.argentwolf.org
On the Relay itself, the policy information will be presented on the default home page:

On the Directory, this information will be reflected as well with time stamps on when it was received and last checked for health by the Directory:

Other relays may be manually added to the Directory by the Directory operator/Admin. I have manually collected a list of Fediverse relays and populated my Directory installation with information about them. These sites are not automatically updated with regard to policy, but are verified as reachable by the Directory server.

Relays that are restricted (Approval Required) or closed to sign ups will appear as such on the Directory. Relays that go offline or are otherwise unreachable will appear in the offline list, eventually moving to the Graveyard after 30 days of offline time. Offline relays can be added the the Directory, but they will not become visible on the index until they pass an online check.

As always, if you want to make your own front facing website, you can disable the built in pages and use your own endpoints!
Comments, feedback, and bug reports are all welcome! Repo links below, available in both .deb and docker images. If you found these useful, please consider a tip to my Ko-fi to support further development! Thank you!
https://github.com/thystra/Activity-Relay
register, heartbeat, and sync commands so they coordinate cleanly with the background scheduler.--test-config validation with safer handling of optional profile errors.SUPPORT information to the generated relay website, including links and plain-text values such as cryptocurrency addresses.linux/amd64 and linux/arm64 at ghcr.io/thystra/activity-relay:3.1.0.https://github.com/thystra/activity-relay-directory
override > relay > csv./v2/relays and the public Directory with profile information, site counts, and Open/Restricted/Closed registration status./v1/relays compatibility API.linux/amd64 at ghcr.io/thystra/activity-relay-directory:1.3.0.Pixelfed is not for sale. It never was.
I've turned down 6 and 7 figure offers, and I'd do it again tomorrow. Some things matter more than a beach and a Peach Bellini.
We're building something generational here. Something that outlives us if we get it right.
100M+ people. Ethically sustainable. Admins and creators get paid.
You deserve better, and we're going to build it together.
🎤 Upcoming at SeaGL 2026:
📍 10:30 AM on October 23
🗣️ "BadgeFed: Open, Decentralized Badges via ActivityPub"
👥 Speaker(s): Maho Pacheco
📍 Room: Room 250
🏷️ Track: Everything Else
📝 Digital badges promise portable, verifiable recognition, but in practice, they’re locked behind prop...
#SeaGL2026 #tech #activitypub #selfhosting #fediverse #linux
🔗 https://pretalx.seagl.org/2026/talk/UC3VVG/
pretalx.seagl.org
Digital badges promise portable, verifiable recognition, but in practice, they’re locked behind proprietary platforms, expensive subscriptions, and rigid workflows. For many nonprofits and open communities, that makes them unusable. BadgeFed rethinks digital credentials for the open web. Built on the Open Badges standard and the ActivityPub protocol, it brings badge issuance, sharing, and verification into the Fediverse, fully open source, self-hostable, and designed to run on Linux. This talk explores how federated social infrastructure can replace centralized credential platform and build sustainable recognition systems without vendor lock-in.
🎤 Upcoming at SeaGL 2026:
📍 10:30 AM on October 23
🗣️ "BadgeFed: Open, Decentralized Badges via ActivityPub"
👥 Speaker(s): Maho Pacheco
📍 Room: Room 250
🏷️ Track: Everything Else
📝 Digital badges promise portable, verifiable recognition, but in practice, they’re locked behind prop...
#SeaGL2026 #tech #activitypub #selfhosting #fediverse #linux
🔗 https://pretalx.seagl.org/2026/talk/UC3VVG/
pretalx.seagl.org
Digital badges promise portable, verifiable recognition, but in practice, they’re locked behind proprietary platforms, expensive subscriptions, and rigid workflows. For many nonprofits and open communities, that makes them unusable. BadgeFed rethinks digital credentials for the open web. Built on the Open Badges standard and the ActivityPub protocol, it brings badge issuance, sharing, and verification into the Fediverse, fully open source, self-hostable, and designed to run on Linux. This talk explores how federated social infrastructure can replace centralized credential platform and build sustainable recognition systems without vendor lock-in.
🎤 Upcoming at SeaGL 2026:
📍 10:30 AM on October 23
🗣️ "BadgeFed: Open, Decentralized Badges via ActivityPub"
👥 Speaker(s): Maho Pacheco
📍 Room: Room 250
🏷️ Track: Everything Else
📝 Digital badges promise portable, verifiable recognition, but in practice, they’re locked behind prop...
#SeaGL2026 #tech #activitypub #selfhosting #fediverse #linux
🔗 https://pretalx.seagl.org/2026/talk/UC3VVG/
pretalx.seagl.org
Digital badges promise portable, verifiable recognition, but in practice, they’re locked behind proprietary platforms, expensive subscriptions, and rigid workflows. For many nonprofits and open communities, that makes them unusable. BadgeFed rethinks digital credentials for the open web. Built on the Open Badges standard and the ActivityPub protocol, it brings badge issuance, sharing, and verification into the Fediverse, fully open source, self-hostable, and designed to run on Linux. This talk explores how federated social infrastructure can replace centralized credential platform and build sustainable recognition systems without vendor lock-in.
OK, who are the experts on #DistributedSystems on the #Fediverse? There is a new #FEP for implementing a scalar #LogicalClock in #ActivityPub, #FEP4f18.
I'm especially interested in mitigations for Byzantine faults in the system; right now, it's kind of hand-wavy based on value and trust heuristics:
helge.codeberg.page
Logical clocks capture the causal ordering of events in a system. This proposal introduces a logical clock system for the ActivityPub network.
Does anyone knows how to hide other Mastodon contact's activity on Friendica's timeline? If I follow any Mastodon account the timeline gets flooded with other people's posts where this contact had any sort of interaction. Is that a known bug?
OK, who are the experts on #DistributedSystems on the #Fediverse? There is a new #FEP for implementing a scalar #LogicalClock in #ActivityPub, #FEP4f18.
I'm especially interested in mitigations for Byzantine faults in the system; right now, it's kind of hand-wavy based on value and trust heuristics:
helge.codeberg.page
Logical clocks capture the causal ordering of events in a system. This proposal introduces a logical clock system for the ActivityPub network.
OK, who are the experts on #DistributedSystems on the #Fediverse? There is a new #FEP for implementing a scalar #LogicalClock in #ActivityPub, #FEP4f18.
I'm especially interested in mitigations for Byzantine faults in the system; right now, it's kind of hand-wavy based on value and trust heuristics:
helge.codeberg.page
Logical clocks capture the causal ordering of events in a system. This proposal introduces a logical clock system for the ActivityPub network.
OK, who are the experts on #DistributedSystems on the #Fediverse? There is a new #FEP for implementing a scalar #LogicalClock in #ActivityPub, #FEP4f18.
I'm especially interested in mitigations for Byzantine faults in the system; right now, it's kind of hand-wavy based on value and trust heuristics:
helge.codeberg.page
Logical clocks capture the causal ordering of events in a system. This proposal introduces a logical clock system for the ActivityPub network.
OK, who are the experts on #DistributedSystems on the #Fediverse? There is a new #FEP for implementing a scalar #LogicalClock in #ActivityPub, #FEP4f18.
I'm especially interested in mitigations for Byzantine faults in the system; right now, it's kind of hand-wavy based on value and trust heuristics:
helge.codeberg.page
Logical clocks capture the causal ordering of events in a system. This proposal introduces a logical clock system for the ActivityPub network.
Interesting. First time I hear about TEPP. Yea, Nostr is not on EC's radar, I guess.
PS. There was a #FEP submission around Guardians recently, for #ActivityPub ..
https://socialhub.activitypub.rocks/t/fep-633c-guardians/8816
socialhub.activitypub.rocks
(in case of feedback) Actor must check, if recepient ‘Guardian’ has guardians and then withhold delivery. If none of the Guardian objects of an actor are free of Guardians? We do not GrandGuardian it (recurse). - we fail softly whispering ‘Not a Teapot’, or something. It is a configuration error to add guardians to a profile that is Guardian So to check, isGuardian should be an extension of the Actor object, preventing the setting of guardians. Set when the Actor is accepted as Guardian and t...
Tomorrow is the submission deadline of an #EuropeanCommission grant for a pilot around youth and decentralized #SocialMedia. Contestants are given the choice to target either the #ActivityPub fediverse, or #ATProto atmosphere. Targeting both is also an option.
Competition is expected to be fierce. It will be quite interesting to see who the EC will pick as the winner, and what protocol their proposal is based on.
It is telling that there is only one pilot for either one or the other #SocialWeb protocol, and not two pilots, each for their own protocol. Though I hope that an AP-based #fediverse pitch will be granted, my gut feeling tells me they probably already made up their mind - driven by political and market(ing) forces - to select #atmosphere. Hope I'm proven wrong. Fediverse is the more "for the people, by the people" choice, and best fit to European culture and values imho, among others.
Results will be in near the end of the year.
ec.europa.eu
The Funding and Tenders Portal is the single entry point (the Single Electronic Data Interchange Area) for applicants, contractors and experts in funding programmes and procurements managed by the European Commission.
Tomorrow is the submission deadline of an #EuropeanCommission grant for a pilot around youth and decentralized #SocialMedia. Contestants are given the choice to target either the #ActivityPub fediverse, or #ATProto atmosphere. Targeting both is also an option.
Competition is expected to be fierce. It will be quite interesting to see who the EC will pick as the winner, and what protocol their proposal is based on.
It is telling that there is only one pilot for either one or the other #SocialWeb protocol, and not two pilots, each for their own protocol. Though I hope that an AP-based #fediverse pitch will be granted, my gut feeling tells me they probably already made up their mind - driven by political and market(ing) forces - to select #atmosphere. Hope I'm proven wrong. Fediverse is the more "for the people, by the people" choice, and best fit to European culture and values imho, among others.
Results will be in near the end of the year.
ec.europa.eu
The Funding and Tenders Portal is the single entry point (the Single Electronic Data Interchange Area) for applicants, contractors and experts in funding programmes and procurements managed by the European Commission.
The Commission of the European Union 🇪🇺 seeks feedback on the proposed #KidsAct until ⏰ 26 November 2026:
👉 https://digital-strategy.ec.europa.eu/en/news/commission-seeks-feedback-eu-kids-act
#ATmospehre #Eurosky #ATProto #ActivityPub #Fediverse #SocialMedia #OpenSocialWeb #OSWA #DiDit #DutGemacht #SocialMediaBriefing #Instagram #TikTok #ChildProtection #OpenCommunication #Meta #AgeVerification #Alterskontrolle
digital-strategy.ec.europa.eu
The Commission is gathering feedback on the proposed EU Kids Act.
Wie könnte sich ein möglicher, sogenannter #KidsAct auf dezentrale #SocialMedia auswirken?
Aktuelle Artikel dazu:
🔹 Stirbt #Mastodon an Europas neuem #Jugendschutz?
@wintermeyer
👉 https://www.heise.de/hintergrund/Altersverifikation-im-Fediverse-Wie-der-KIDS-Act-Mastodon-herausfordert-11474098.html
🔹 Bye Bye #Fediverse @epicenter_works :
👉 https://epicenter.works/content/zwei-fronten-ein-irrweg-warum-digitaler-ausweiszwang-niemanden-schuetzt
🔹 EU-Kommission will Netz mit #Alterskontrolle zupflastern
@sebmeineck
👉 https://netzpolitik.org/2026/eu-kids-act-eu-kommission-will-netz-mit-alterskontrollen-zupflastern/
➡️ Die EU-Kommission bittet bis 26. Nov. um Feedback:
👉 https://digital-strategy.ec.europa.eu/en/news/commission-seeks-feedback-eu-kids-act
digital-strategy.ec.europa.eu
The Commission is gathering feedback on the proposed EU Kids Act.
The Commission of the European Union 🇪🇺 seeks feedback on the proposed #KidsAct until ⏰ 26 November 2026:
👉 https://digital-strategy.ec.europa.eu/en/news/commission-seeks-feedback-eu-kids-act
#ATmospehre #Eurosky #ATProto #ActivityPub #Fediverse #SocialMedia #OpenSocialWeb #OSWA #DiDit #DutGemacht #SocialMediaBriefing #Instagram #TikTok #ChildProtection #OpenCommunication #Meta #AgeVerification #Alterskontrolle
digital-strategy.ec.europa.eu
The Commission is gathering feedback on the proposed EU Kids Act.
Wie könnte sich ein möglicher, sogenannter #KidsAct auf dezentrale #SocialMedia auswirken?
Aktuelle Artikel dazu:
🔹 Stirbt #Mastodon an Europas neuem #Jugendschutz?
@wintermeyer
👉 https://www.heise.de/hintergrund/Altersverifikation-im-Fediverse-Wie-der-KIDS-Act-Mastodon-herausfordert-11474098.html
🔹 Bye Bye #Fediverse @epicenter_works :
👉 https://epicenter.works/content/zwei-fronten-ein-irrweg-warum-digitaler-ausweiszwang-niemanden-schuetzt
🔹 EU-Kommission will Netz mit #Alterskontrolle zupflastern
@sebmeineck
👉 https://netzpolitik.org/2026/eu-kids-act-eu-kommission-will-netz-mit-alterskontrollen-zupflastern/
➡️ Die EU-Kommission bittet bis 26. Nov. um Feedback:
👉 https://digital-strategy.ec.europa.eu/en/news/commission-seeks-feedback-eu-kids-act
digital-strategy.ec.europa.eu
The Commission is gathering feedback on the proposed EU Kids Act.
Wie könnte sich ein möglicher, sogenannter #KidsAct auf dezentrale #SocialMedia auswirken?
Aktuelle Artikel dazu:
🔹 Stirbt #Mastodon an Europas neuem #Jugendschutz?
@wintermeyer
👉 https://www.heise.de/hintergrund/Altersverifikation-im-Fediverse-Wie-der-KIDS-Act-Mastodon-herausfordert-11474098.html
🔹 Bye Bye #Fediverse @epicenter_works :
👉 https://epicenter.works/content/zwei-fronten-ein-irrweg-warum-digitaler-ausweiszwang-niemanden-schuetzt
🔹 EU-Kommission will Netz mit #Alterskontrolle zupflastern
@sebmeineck
👉 https://netzpolitik.org/2026/eu-kids-act-eu-kommission-will-netz-mit-alterskontrollen-zupflastern/
➡️ Die EU-Kommission bittet bis 26. Nov. um Feedback:
👉 https://digital-strategy.ec.europa.eu/en/news/commission-seeks-feedback-eu-kids-act
digital-strategy.ec.europa.eu
The Commission is gathering feedback on the proposed EU Kids Act.
Tomorrow is the submission deadline of an #EuropeanCommission grant for a pilot around youth and decentralized #SocialMedia. Contestants are given the choice to target either the #ActivityPub fediverse, or #ATProto atmosphere. Targeting both is also an option.
Competition is expected to be fierce. It will be quite interesting to see who the EC will pick as the winner, and what protocol their proposal is based on.
It is telling that there is only one pilot for either one or the other #SocialWeb protocol, and not two pilots, each for their own protocol. Though I hope that an AP-based #fediverse pitch will be granted, my gut feeling tells me they probably already made up their mind - driven by political and market(ing) forces - to select #atmosphere. Hope I'm proven wrong. Fediverse is the more "for the people, by the people" choice, and best fit to European culture and values imho, among others.
Results will be in near the end of the year.
ec.europa.eu
The Funding and Tenders Portal is the single entry point (the Single Electronic Data Interchange Area) for applicants, contractors and experts in funding programmes and procurements managed by the European Commission.
Fediverse Software Distribution (from https://fedidb.com)
After some discussion, I added a #FEP for a scalar #LogicalClock for the #ActivityPub network.
I'm excited to try this out; I think it can have some interesting uses.
codeberg.org
[Logical clocks](https://en.wikipedia.org/wiki/Logical_clock) capture the causal ordering of events in a distributed system. This proposal introduces a logical clock system for the ActivityPub network.
Pixelfed is not for sale. It never was.
I've turned down 6 and 7 figure offers, and I'd do it again tomorrow. Some things matter more than a beach and a Peach Bellini.
We're building something generational here. Something that outlives us if we get it right.
100M+ people. Ethically sustainable. Admins and creators get paid.
You deserve better, and we're going to build it together.
Fediverse Software Distribution (from https://fedidb.com)
Fediverse Software Distribution (from https://fedidb.com)
hyperblog.de/hoergen/en/posts/…
#demhoergen #hyperblog #fediverse #mastodon #friendica #activitypub #digital sovereignty #data protection #decentralization
An assessment framework for management and decision-makers - why the Fediverse answers the strategic question of the communication platform anew and which criteria really matter.
hyperblog.de/hoergen/en/posts/…
#demhoergen #hyperblog #fediverse #mastodon #friendica #activitypub #digital sovereignty #data protection #decentralization
An assessment framework for management and decision-makers - why the Fediverse answers the strategic question of the communication platform anew and which criteria really matter.
hyperblog.de/hoergen/en/posts/…
#demhoergen #hyperblog #fediverse #mastodon #friendica #activitypub #digital sovereignty #data protection #decentralization
An assessment framework for management and decision-makers - why the Fediverse answers the strategic question of the communication platform anew and which criteria really matter.
hyperblog.de/hoergen/en/posts/…
#demhoergen #hyperblog #fediverse #mastodon #friendica #activitypub #digital sovereignty #data protection #decentralization
An assessment framework for management and decision-makers - why the Fediverse answers the strategic question of the communication platform anew and which criteria really matter.
hyperblog.de/hoergen/en/posts/…
#demhoergen #hyperblog #fediverse #mastodon #friendica #activitypub #digital sovereignty #data protection #decentralization
An assessment framework for management and decision-makers - why the Fediverse answers the strategic question of the communication platform anew and which criteria really matter.

gitflic.ru
Personal fediverse management
Of all the ActivityPub servers that have ever been publicly available, how many do you think are still up today?
Servers
- PeerTube v8.3.1
- Hollo v0.9.20
- Mastodon v4.7.3
- Ibis v0.3.4
- Wafrn v2026.10.01
- Hometown v1.3.0
- Misskey v2026.10.0
- NeoDB v0.19.5
- Hatsu v0.3.5
- Wanderer v0.21.0
Clients
- Pachli v3.9.0
- Fedilab v3.45.0
- chan-fe v0.4.0
- PeerTube Mobile v2.3.0
For developers
- Fedify v2.4.0
- activitypub-federation-rust v0.6.6
- Fedipub v0.10.0
Protocol
- FEP-070c: Block synchronization across servers
- FEP-302f: QuantPub — federated personal metrics for quantified-self sharing
- FEP-6757: Content License Metadata in ActivityPub
Articles
- Why Does Nobody Use Lemmy?
- Fediverse News – Summer 2026
-----
#WeekInFediverse #Fediverse #ActivityPub
Previous edition: https://mitra.social/objects/01a0dbf1-5095-74a1-b198-2872b783a198
mitra.social
Week in Fediverse 2026-09-25 - Post by @weekinfediverse
Servers
- PeerTube v8.3.1
- Hollo v0.9.20
- Mastodon v4.7.3
- Ibis v0.3.4
- Wafrn v2026.10.01
- Hometown v1.3.0
- Misskey v2026.10.0
- NeoDB v0.19.5
- Hatsu v0.3.5
- Wanderer v0.21.0
Clients
- Pachli v3.9.0
- Fedilab v3.45.0
- chan-fe v0.4.0
- PeerTube Mobile v2.3.0
For developers
- Fedify v2.4.0
- activitypub-federation-rust v0.6.6
- Fedipub v0.10.0
Protocol
- FEP-070c: Block synchronization across servers
- FEP-302f: QuantPub — federated personal metrics for quantified-self sharing
- FEP-6757: Content License Metadata in ActivityPub
Articles
- Why Does Nobody Use Lemmy?
- Fediverse News – Summer 2026
-----
#WeekInFediverse #Fediverse #ActivityPub
Previous edition: https://mitra.social/objects/01a0dbf1-5095-74a1-b198-2872b783a198
mitra.social
Week in Fediverse 2026-09-25 - Post by @weekinfediverse
Servers
- PeerTube v8.3.1
- Hollo v0.9.20
- Mastodon v4.7.3
- Ibis v0.3.4
- Wafrn v2026.10.01
- Hometown v1.3.0
- Misskey v2026.10.0
- NeoDB v0.19.5
- Hatsu v0.3.5
- Wanderer v0.21.0
Clients
- Pachli v3.9.0
- Fedilab v3.45.0
- chan-fe v0.4.0
- PeerTube Mobile v2.3.0
For developers
- Fedify v2.4.0
- activitypub-federation-rust v0.6.6
- Fedipub v0.10.0
Protocol
- FEP-070c: Block synchronization across servers
- FEP-302f: QuantPub — federated personal metrics for quantified-self sharing
- FEP-6757: Content License Metadata in ActivityPub
Articles
- Why Does Nobody Use Lemmy?
- Fediverse News – Summer 2026
-----
#WeekInFediverse #Fediverse #ActivityPub
Previous edition: https://mitra.social/objects/01a0dbf1-5095-74a1-b198-2872b783a198
mitra.social
Week in Fediverse 2026-09-25 - Post by @weekinfediverse
Servers
- PeerTube v8.3.1
- Hollo v0.9.20
- Mastodon v4.7.3
- Ibis v0.3.4
- Wafrn v2026.10.01
- Hometown v1.3.0
- Misskey v2026.10.0
- NeoDB v0.19.5
- Hatsu v0.3.5
- Wanderer v0.21.0
Clients
- Pachli v3.9.0
- Fedilab v3.45.0
- chan-fe v0.4.0
- PeerTube Mobile v2.3.0
For developers
- Fedify v2.4.0
- activitypub-federation-rust v0.6.6
- Fedipub v0.10.0
Protocol
- FEP-070c: Block synchronization across servers
- FEP-302f: QuantPub — federated personal metrics for quantified-self sharing
- FEP-6757: Content License Metadata in ActivityPub
Articles
- Why Does Nobody Use Lemmy?
- Fediverse News – Summer 2026
-----
#WeekInFediverse #Fediverse #ActivityPub
Previous edition: https://mitra.social/objects/01a0dbf1-5095-74a1-b198-2872b783a198
mitra.social
Week in Fediverse 2026-09-25 - Post by @weekinfediverse
@dansup @hongminhee I made a PR that I think captures all the errors you two reported.
https://codeberg.org/fediverse/fep/pulls/943
I thought I'd be able to slip it into the original PR, but @FenTiger landed the original really quickly. 🙏🏼
codeberg.org
This patch fixes a number of syntax errors and identifier typos reported by @dansup and others and summarized by @hongminhee in https://codeberg.org/evanp/fep/issues/52 - <https://schema.org/#license> -> <https://schema.org/license> - <https://creativecommons.org/ns#license> -> <http://creative...
Of all the ActivityPub servers that have ever been publicly available, how many do you think are still up today?
@fabio support for live streams now available in #Songhive.
Spin up your instance, upload your songs, create an HTTP stream mount point, queue your songs there from the Web player, and share a link so friends can tune in to your radio.
Or, if you want to directly point your audio player to the direct stream:
manganiello.music/streams/fabio.mp3
Big shout to @nemo who has also been testing Songhive lately. He has an account @nemo and a night time radio stream too 😆
manganiello.eu
@cypherhippie at @republica in #Vienna giving a talk about "Leaving Planet X - Howto Dis-Enshittify Public Online Spaces Fast"
Open protocols based networks like #ActivityPub and #ATproto make that possible.
@fabio support for live streams now available in #Songhive.
Spin up your instance, upload your songs, create an HTTP stream mount point, queue your songs there from the Web player, and share a link so friends can tune in to your radio.
Or, if you want to directly point your audio player to the direct stream:
manganiello.music/streams/fabio.mp3
Big shout to @nemo who has also been testing Songhive lately. He has an account @nemo and a night time radio stream too 😆
manganiello.eu
@cypherhippie at @republica in #Vienna giving a talk about "Leaving Planet X - Howto Dis-Enshittify Public Online Spaces Fast"
Open protocols based networks like #ActivityPub and #ATproto make that possible.
I finally created a #FEP for using #CreativeCommons licenses (and other licenses) in #ActivityPub.
codeberg.org
An explicit copyright license on ActivityPub content objects makes it easier to re-use and re-distribute content. This FEP describes properties for content objects that identify the license for that object, and a property for actors to define their preferred license. It uses a term from metadata ...
Nobody owns email. Nobody should own your social life either.
When you want to show someone why they should join the fediverse, show them this:
fediverse.info
A friendly guide to the open social web.
Nobody owns email. Nobody should own your social life either.
When you want to show someone why they should join the fediverse, show them this:
fediverse.info
A friendly guide to the open social web.
I finally created a #FEP for using #CreativeCommons licenses (and other licenses) in #ActivityPub.
codeberg.org
An explicit copyright license on ActivityPub content objects makes it easier to re-use and re-distribute content. This FEP describes properties for content objects that identify the license for that object, and a property for actors to define their preferred license. It uses a term from metadata ...
I finally created a #FEP for using #CreativeCommons licenses (and other licenses) in #ActivityPub.
codeberg.org
An explicit copyright license on ActivityPub content objects makes it easier to re-use and re-distribute content. This FEP describes properties for content objects that identify the license for that object, and a property for actors to define their preferred license. It uses a term from metadata ...
I finally created a #FEP for using #CreativeCommons licenses (and other licenses) in #ActivityPub.
codeberg.org
An explicit copyright license on ActivityPub content objects makes it easier to re-use and re-distribute content. This FEP describes properties for content objects that identify the license for that object, and a property for actors to define their preferred license. It uses a term from metadata ...
I finally created a #FEP for using #CreativeCommons licenses (and other licenses) in #ActivityPub.
codeberg.org
An explicit copyright license on ActivityPub content objects makes it easier to re-use and re-distribute content. This FEP describes properties for content objects that identify the license for that object, and a property for actors to define their preferred license. It uses a term from metadata ...
I finally created a #FEP for using #CreativeCommons licenses (and other licenses) in #ActivityPub.
codeberg.org
An explicit copyright license on ActivityPub content objects makes it easier to re-use and re-distribute content. This FEP describes properties for content objects that identify the license for that object, and a property for actors to define their preferred license. It uses a term from metadata ...
I finally created a #FEP for using #CreativeCommons licenses (and other licenses) in #ActivityPub.
codeberg.org
An explicit copyright license on ActivityPub content objects makes it easier to re-use and re-distribute content. This FEP describes properties for content objects that identify the license for that object, and a property for actors to define their preferred license. It uses a term from metadata ...
I finally created a #FEP for using #CreativeCommons licenses (and other licenses) in #ActivityPub.
codeberg.org
An explicit copyright license on ActivityPub content objects makes it easier to re-use and re-distribute content. This FEP describes properties for content objects that identify the license for that object, and a property for actors to define their preferred license. It uses a term from metadata ...
There's a new very lightweight Fediverse platform that's just been released called littleFedi. You can follow its new official account at:
Its official website including download and source code is at:
LittleFedi is for techy people who want to create their own Fediverse server using as few resources as possible.
It's written by a group called "Little Computer People", its maintainer is @stefano
littlefedi.org
A small ActivityPub server written in Go. One binary, no runtime, no external services.
Fedify 2.4.0 is out.
The main addition is support for FEP-ef61 portable objects. A Fedify server can now host actors whose identity is a DID instead of a domain, serve their signed objects through /.well-known/apgateway/, and verify the portable objects of other servers. I tested it against tootik and Mitra, and wrote up where Fedify's profile differs from the FEP and what it leaves out.
The release also adds application-defined background tasks, the ActivityPub Media Upload extension, per-request inbox reports, 410 Gone for deleted objects, and four new packages (AdonisJS, Netlify, PGlite, and interaction controls).
Some defaults changed, so please read the upgrading section before you update, especially if you federate with software that uses FEP-ef61 compatible identifiers.
Release notes: https://hackers.pub/@fedify/2026/fedify-2-4.
Thanks to everyone who contributed to this cycle, and to @silverpill for patient answers about the FEP.
hackers.pub
Fedify is a TypeScript framework for building ActivityPub servers. It implements federation details such as HTTP Signatures, JSON-LD processing, WebFinger, inbox and outbox routing, and activity delivery.
Fedify 2.4.0 is a minor release with an unusually long changelog. The main addition is support for FEP-ef61 portable objects: a Fedify server can now host actors whose identity is a DID rather than a domain, serve and verify their signed objects, and consume the portable objects of other servers. The release also adds an API for application-defined background tasks, the ActivityPub Media Upload extension, a hook that reports how every inbox request was authenticated and handled, and 410 Gone tombstones for deleted objects. Several defaults change to limit what a hostile peer can make Fedify do. Four new packages ship with this version: @fedify/adonisjs, @fedify/interaction-controls, @fedify/netlify, and @fedify/pglite.
Most applications can upgrade without code changes, but a few of the new defaults are behavior changes. They are listed in the Upgrading section at the end.
An ActivityPub object's ID is an HTTP(S) URL on its server's domain. If that server shuts down, its IDs stop resolving, and an actor that moves elsewhere gets a new ID that everyone has to follow again. FEP-ef61, written by @silverpill of Mitra, proposes IDs that do not name a server at all. A portable ID is an ap: URI whose authority is a DID:
ap://did:key:z6MkrJVnaZkeFzdQyMZu1cgjg7k1pZZ6pvBQ7XJPt4swbTQ2/actor
The same object can be stored on several servers, called gateways, each of which serves it under /.well-known/apgateway/. Because no server owns the ID, a portable actor, activity, or object is authenticated only by its FEP-8b32 Object Integrity Proof, made with a key of the DID in its ID. A portable actor lists its gateways in a gateways property, and other servers deliver to its inbox through them.
Fedify 2.4.0 implements a profile of FEP-ef61 for gateways and consumers of portable objects. It makes two deliberate choices where the FEP or the specifications it rests on leave questions open, and it leaves out a few parts of the proposal; both are described below. The new Portable objects chapter of the manual walks through running portable actors from start to finish and then describes the profile as a whole, so that other implementers can tell what to expect from a Fedify server. The request came from @anotherdoesnm in #288, which tracked the work from a roadmap to the final pieces.
Portable objects are a foundation for nomadic identity, not nomadic identity itself. Fedify serves and verifies portable objects and forwards activities between gateways, but it does not copy actors or posts between servers, rotate keys, or move an actor to another DID. Full nomadic identity is still open in #413.
A portable actor starts with an Ed25519 key pair. Its DID is made from the public key with exportDidKey(), and whoever holds the private key can act as the actor, so store it with the actor and keep it safe:
import { generateCryptoKeyPair } from "@fedify/fedify";
import { exportDidKey } from "@fedify/vocab-runtime";
const { publicKey, privateKey } = await generateCryptoKeyPair("Ed25519");
const did = await exportDidKey(publicKey); // did:key:z6Mk...
// The verification method that the actor's proofs refer to:
const didKeyId = new URL(`${did}#${did.slice("did:key:".length)}`);
The actor dispatcher returns a portable actor as it would an ordinary one. Context.getPortableActorUri() and its siblings for inboxes, outboxes, collections, and objects build portable IDs from the dispatcher's paths and the DID, and signObject() signs the actor with the DID's key:
federation
.setActorDispatcher("/users/{identifier}", async (ctx, identifier) => {
const user = await findUser(identifier);
if (user == null) return null;
// A request through the gateway endpoint names the DID in its path,
// e.g., GET /.well-known/apgateway/did:key:z6Mk.../users/alice:
if (
ctx.portableRequest != null &&
ctx.portableRequest.authority !== user.did
) {
return null;
}
// This server's gateway keys for the actor (see below):
const keys = await ctx.getActorKeyPairs(identifier);
return await signObject(
new Person({
id: ctx.getPortableActorUri(identifier, user.did), // ap+ef61://did:key:z6Mk.../users/alice
preferredUsername: user.username,
inbox: ctx.getPortableInboxUri(identifier, user.did),
outbox: ctx.getPortableOutboxUri(identifier, user.did),
followers: ctx.getPortableFollowersUri(identifier, user.did),
gateways: [new URL("https://example.com")],
publicKeys: keys.map((key) => key.cryptographicKey),
assertionMethods: keys.map((key) => key.multikey),
}),
user.didKeyPair.privateKey,
new URL(`${user.did}#${user.did.slice("did:key:".length)}`),
);
})
.setKeyPairsDispatcher(async (ctx, identifier) => {
const user = await findUser(identifier);
// This server's own key pair for the actor, not the DID's:
return user == null ? [] : [user.gatewayKeyPair];
})
.mapPortableActorId(async (ctx, identifier) => {
const user = await findUser(identifier);
return user == null ? null : ctx.getPortableActorUri(identifier, user.did);
});
With this dispatcher, Fedify serves the actor at its compatible identifier, https://example.com/.well-known/apgateway/did:key:z6Mk.../users/alice, which is how other servers retrieve it. It serves it through WebFinger as @alice@example.com, since the handle's domain comes from the first gateway in gateways, and at its ordinary URL as before. On the gateway endpoint, Fedify serves a document only if its ID is the requested portable ID and it carries a proof made with a key of that DID.
The key pairs dispatcher and the new ActorCallbackSetters.mapPortableActorId() give the actor gateway keys. These are the server's own keys for the actor, and they sign only the HTTP requests the server makes on the actor's behalf, such as deliveries. They never sign the actor's activities or objects; only the DID's key does that. The actor document lists them under assertionMethods, as FEP-521a describes, so that other servers can verify the HTTP Signatures. See Dispatching the actor and Gateway keys of portable actors for the details.
Object and collection dispatchers serve portable objects and collections through the gateway endpoint in the same way, routed by the path after the DID. Two rules protect the actor's data. A non-public portable object is served only if the dispatcher has an authorization predicate, because FEP-ef61 forbids a gateway to show it to anyone outside its audience; the new RequestContext.isSignedByAudience() helps to write one. Without a predicate, a request for such an object gets 404 Not Found, as though the gateway did not store it. A deleted object is served as a signed Tombstone with 410 Gone. Resources that portable objects refer to by SHA-256 hashlinks, such as GET /.well-known/apgateway/hl:zQm..., are served by the new Federatable.setHashlinkMediaDispatcher().
A portable actor's activities have to be signed by its DID, which Fedify's key pairs dispatcher does not know about. Sign them with signObject() before sending, and pass normalizeExistingProofs: true so that the activity goes out in the form its proof covers. An activity that embeds a portable object, such as the Note of a Create, is signed inside out:
const actor = withGatewayHints(
ctx.getPortableActorUri(user.identifier, user.did),
[new URL("https://example.com")],
); // ap+ef61://did:key:z6Mk.../users/alice?@gateway=https%3A%2F%2Fexample.com
const note = await signObject(
new Note({
id: ctx.getPortableObjectUri(
Note,
{ identifier: user.identifier, id: crypto.randomUUID() },
user.did,
),
attribution: actor,
to: PUBLIC_COLLECTION,
content: "Hello, world!",
}),
user.didKeyPair.privateKey,
didKeyId,
);
const create = await signObject(
new Create({
id: ctx.getPortableObjectUri(
Note,
{ identifier: user.identifier, id: crypto.randomUUID() },
user.did,
),
actor,
to: PUBLIC_COLLECTION,
object: note,
}),
user.didKeyPair.privateKey,
didKeyId,
);
await ctx.sendActivity(
{ identifier: user.identifier },
"followers",
create,
{ normalizeExistingProofs: true },
);
This example depends on a fix to signObject() that is useful outside FEP-ef61 as well. Previously, a signed object assigned to a typed parent was rebuilt under the parent's JSON-LD context when the parent was serialized. The child kept its proofValue but lost the document its proof covered, so it no longer verified on its own. signObject() now captures the exact document it signed, and nested serialization embeds that document verbatim. The captured document is a snapshot: clone() does not carry it, so sign a clone again if it has to be embedded. The design is in #1044 and the fix in #1051.
Context.sendActivity() refuses a portable activity that is unsigned, signed by the wrong key, or carries a Linked Data Signature, and throws a TypeError before anything is queued. It also refuses a few shapes of compound document that Fedify's own inboxes would reject, such as a received signed Follow embedded in an Accept; refer to such an object by its ID instead. Deliveries to portable inboxes go through the recipient's gateways, or else the @gateway hints on the inbox, trying at most five of them until one accepts. Before this release, they failed with UrlError: Unsupported protocol: ap+ef61:. The rules are in Delivering to portable actors and Producing a compound document.
On the receiving side, a portable inbox is reached as POST /.well-known/apgateway/did:key:z6Mk.../users/alice/inbox and dispatched to the same inbox listeners as the ordinary inbox. Context.parseUri() gains a portable option, and the parsed result has the DID in a new authority property to check against the one you store. Fedify accepts an activity of a portable actor, or one with a portable ID, only if it has a valid proof by that DID; neither an HTTP Signature nor a Linked Data Signature authenticates it. Each portable object embedded in the activity is verified against its own proof before dispatch. An accepted activity that carries its own proof is forwarded to the actor's other gateways at most once, as FEP-ef61 recommends. Forwarding needs a key–value store that supports cas(), and the FederationOptions.portableInboxForwarding option tunes it. See Portable inboxes and Forwarding to other gateways.
Applications that never host a portable actor still meet portable objects from other servers. Context.lookupObject() resolves portable IDs, compatible identifiers, and the fediverse handles of portable actors, and returns portable objects only if they verify:
const actor = await ctx.lookupObject("@alice@example.com");
// A portable ID without @gateway hints needs gateways to ask:
const note = await ctx.lookupObject("ap://did:key:z6Mk.../notes/1", {
gateways: ["https://example.com"],
});
Property accessors fetch portable references through gateways as well, and they verify what they fetch with the new verifyPortableObject option. Context has a matching verifyPortableObject property, so passing a context as the options, as in await create.getObject(ctx), is enough. Activities received in inboxes use this verifier by default, and objects fetched through their accessors inherit it, so a chained call such as (await create.getObject(ctx))?.getAttribution() verifies each portable object along the way. A lookup asks at most five gateways, falling back to the next when one fails or serves something that does not verify. Unsigned collections are accepted only from a gateway that the owner lists, and only for an actor's inbox, outbox, followers, following, and liked, as FEP-ef61 allows. fetchPortableMedia() retrieves the media of a portable object and returns it only if it matches the object's digestMultibase. See Dereferencing portable references and Default verifiers.
Software that does not understand ap: URIs can refer to a portable object by a compatible identifier, an HTTP(S) URL under a gateway's /.well-known/apgateway/ path such as https://gw.example/.well-known/apgateway/did:key:z6Mk.../actor. Some implementations, tootik among them, use compatible identifiers as the IDs of their portable actors.
Earlier versions of Fedify treated these URLs as ordinary web URLs and trusted them by the server that served them. Since anyone can serve a compatible identifier for any DID, https://evil.example/.well-known/apgateway/did:key:z6MkAlice/actor could act as Alice without a proof by Alice's DID. Fedify 2.4.0 treats a compatible identifier as the portable object it stands for, as FEP-ef61 requires. Inboxes reject an activity from an actor whose ID is a compatible identifier with 401 Unauthorized unless it has a valid Object Integrity Proof by the actor's DID, and there is no option to turn this off. Actors that publish compatible identifiers without signing them are no longer accepted, and neither are those whose DIDs use key types that Fedify cannot verify yet. The change was tracked in #1093 and made in #1105.
Your own portable actors may still use compatible identifiers as their IDs, for software that rejects ap: IDs. Build them with toCompatibleEf61Id() before signing, and Fedify will treat them as portable wherever it serves or sends them. It also warns if one is malformed or does not point at the actor's first gateway. See Compatible identifiers as actor IDs.
Two choices in Fedify's profile differ from the current FEP text or rest on semantics that are not settled yet. Fedify keeps both for the 2.x series, and either may change in Fedify 3.0.
The first is the scheme. FEP-ef61 recommends ap: and canonicalizes ap+ef61: to it, while also warning that the recommendation may change to ap+ef61:, since these IDs are meant only for portable objects. Fedify accepts both, compares them as equal, and serializes ap+ef61:. Parsing a document normalizes its portable IRIs to ap+ef61:, so re-serializing an object that another implementation signed with ap: IDs breaks its proof. Keep the received JSON of such documents, and compare portable IDs with arePortableUrisEqual() rather than as strings. The discussion is in #826 and #828, and the reasoning is in Canonical ap+ef61: scheme.
The second concerns compound documents. When a signed Note sits inside a signed Create, the verifier has to know which part of the JSON each proof covers, and neither FEP-8b32 nor Verifiable Credential Data Integrity defines that yet; the question is open in w3c/vc-data-integrity#350. Fedify uses an interim map-local profile, worked out in #938. Each JSON map with a proof is its own secured document, each embedded portable object needs its own proof and its own @context, and proof sets and chains are not supported. The profile authenticates the exact JSON values each proof covers; it does not promise that a child's JSON-LD expansion inside its parent matches its expansion on its own. @silverpill, @dmitrizagidulin, and @by_caballero reviewed the proposal in that thread, and @silverpill reproduced its test vectors independently. See Map-local compound proofs.
Fedify does not implement FEP-ae97, the companion proposal for client-side signing. There are no endpoints for registering actors, submitting client-signed activities to a portable outbox, or gateway discovery, and no media upload and deletion endpoints under /.well-known/apgateway-media. Your own outbox listeners can still relay activities that a client signed, unchanged. Fedify also does not copy or reconcile actors, objects, or collections between gateways, so running an actor on several gateways means keeping their data in sync yourself. There is no workflow for rotating a DID's key or migrating an actor to another DID. Fedify resolves verification methods only for did:key DIDs with Ed25519 keys; other DID methods verify only if your document loader resolves them. Gateways must be plain origins, since the arbitrary gateway paths that FEP-ef61 discusses are not supported. The full list is in Not supported.
The profile was tested against tootik v0.25.4 and Mitra v5.10.0 in #1202. Fedify verifies tootik's portable actors, activities, and HTTP Signatures, and Mitra's gateway HTTP Signatures on activities that a client signed. Both accept the portable activities that Fedify sends. Two fixes came out of this testing. Fedify now reads a gateways list that tootik leaves unmapped in its JSON-LD context. It also serializes gateways as bare origins, because Mitra rejects a trailing slash. Fedify has not yet verified an actor or activity that Mitra signed itself, and Mastodon's handling of portable actors has not been tested. The FEP-ef61 interoperability section of FEDERATION.md records what was tested and how.
fedify lookup looks up portable objects by their ap: and ap+ef61: IDs, compatible identifiers, and the handles of portable actors, and it verifies their proofs. When no gateway returns an acceptable object, it says why. A new --gateway option on fedify lookup, fedify inbox, and fedify webfinger names the gateways to ask. fedify inbox --follow follows portable actors, and fedify webfinger accepts portable actor IDs. See Looking up portable objects in the CLI manual.
The mock federation and contexts in @fedify/testing support portable IDs, verification against fixture document loaders, gateway requests, and hashlink media, so tests can exercise portable objects without a live gateway. @fedify/lint's actor URI rules accept the portable IDs that the getPortable*Uri() methods build. @fedify/next now passes hashlink media requests to Fedify, a problem reported in #1149 and fixed in #1170; to have Next.js run the middleware for them, add { source: "/.well-known/apgateway/:path*" } to your matcher.
Fedify already runs activity delivery and inbox processing on queue workers, but applications usually have jobs of their own: digest emails, timeline rebuilds, link previews. Until now, those jobs could not use Fedify's queues, serialization, or retries. Fedify 2.4.0 opens the same machinery to application-defined tasks.
A task is defined once with defineTask() and enqueued from any Context with enqueueTask() or enqueueTaskMany(). Every task takes a Standard Schema, such as a Zod or Valibot schema, which types its payload. Fedify validates the payload when it is enqueued and again when a worker picks it up; the second check catches payloads from an earlier deployment that no longer match the schema:
import { createExponentialBackoffPolicy } from "@fedify/fedify";
import { z } from "zod";
const sendDigest = federation.defineTask("sendDigest", {
schema: z.object({ userId: z.string(), since: z.date() }),
handler: async (ctx, data) => {
// data is typed as { userId: string; since: Date }
await sendEmail(data.userId, await buildDigest(data.userId, data.since));
},
retryPolicy: createExponentialBackoffPolicy({ maxAttempts: 3 }),
});
// Anywhere with a Context:
await ctx.enqueueTask(sendDigest, { userId: "alice", since: new Date() }, {
delay: { minutes: 30 },
deduplicationKey: "digest:alice",
});
Fedify serializes payloads with devalue, so Date, Map, Set, URL, bigint, Temporal values, circular references, and Activity Vocabulary objects all survive the trip through any message queue backend. A Note in a payload comes back as a Note. Failed handlers are retried with exponential backoff by default, and queues with native retries handle retries themselves. Tasks fall back to the outbox queue unless you give them their own. Use the new task slot of the queue option or a per-task queue for that, and startQueue(ctxData, { queue: "task" }) runs a task-only worker. Setting taskQueueResolution: "strict" turns the fallback into an error.
A deduplicationKey deduplicates enqueues. The feature came from @julian's point in #206 that recurring tasks are hard to manage when you cannot tell whether one is already queued. A queue that declares nativeDeduplication enforces the key itself; otherwise, Fedify keeps a best-effort marker in the key–value store through cas(). Each task runs in a fedify.task span, and the queue metrics from 2.3.0 now include task queues. The Background tasks chapter covers routing, retries, deduplication, and the limitations; cron-style scheduling, result backends, and priorities are out of scope for now.
ChanHaeng Lee (@2chanhaeng) implemented the API: the core in #803, deduplication in #806, and observability in #812, following the plans in #797, #798, and #799. The work was merged in #923, closing #206, which had been open since February 2025.
Fedify 2.2.0 added client-to-server outbox listeners, but a C2S client also has to upload images and video before it can post them. That traffic does not go through POST /outbox. Fedify 2.4.0 supports the ActivityPub Media Upload extension, a SocialCG proposal last updated in 2017 that never reached W3C Recommendation status.
setMediaUploader() registers a multipart/form-data endpoint. Its callback receives the uploaded file and the object shell the client posted along with it. If the callback returns the created object, Fedify responds with 201 Created; if it returns a URL where the object will appear once processing finishes, Fedify responds with 202 Accepted:
federation
.setMediaUploader(
"/users/{identifier}/media",
async (ctx, identifier, file, object) => {
const stored = await uploadToStorage(file);
return new Image({
id: ctx.getObjectUri(Image, { uuid: stored.uuid }),
url: new URL(stored.publicUrl),
mediaType: file.type,
name: object.name,
});
},
)
.authorize(async (ctx, identifier) => {
const session = await verifyAccessToken(
ctx.request.headers.get("authorization"),
);
return session?.identifier === identifier;
});
The actor dispatcher advertises the endpoint itself by setting endpoints: new Endpoints({ uploadMedia: ctx.getMediaUploaderUri(identifier) }), the same way it places its inbox and outbox URIs. Without an authorize() hook, anyone who finds the endpoint can upload to it. For that reason, Fedify logs a warning when an uploader is registered without one, and when the returned object's URI does not match an object dispatcher or the actor does not advertise the endpoint. Four new @fedify/lint rules catch the same mistakes before they run. Fedify reads the upload into memory and sets no size limit of its own, so cap the request size at your reverse proxy. Storing the file and serving the object back are up to the application. See the Media upload chapter; the work is in #754 and #927.
Fedify decides several things about every inbox delivery: which signature mechanisms applied, which keys they tried, whether the actor owned the key, and what happened to the activity. Before this release, applications saw only part of that. A Linked Data Signature by a key the actor did not own was counted the same as a bad signature, and the key that verified a request was never handed to the application. The request in #1191 came from an effort to log these decisions for a debugging tool.
The new onRequestFinished() callback on inbox listeners receives an InboxRequestReport for every delivery to an inbox route, including rejected requests and requests that fail before verification:
federation
.setInboxListeners("/users/{identifier}/inbox", "/inbox")
.on(Follow, async (ctx, follow) => { /* ... */ })
.onRequestFinished(async (ctx, report) => {
await db.insertInboxLog({
inbox: report.inbox.kind, // "personal" | "shared" | "portable"
authentication: report.authentication.status,
outcome: report.outcome.type === "response"
? report.outcome.disposition // "processed" | "enqueued" | "duplicate" | ...
: `exception:${report.outcome.stage}`,
keyIds: report.attempts
.flatMap((attempt) => attempt.checks)
.flatMap((check) => check.triedKeys)
.map((key) => key.id?.href),
});
});
Each HTTP Signature, Linked Data Signature, or Object Integrity Proof check appears as an attempt with the actual keys it tried, including a stale cached key and its refreshed replacement. The report also carries the final authentication decision, such as actorKeyMismatch or proofPolicy, and the response or exception that ended the request. Fedify awaits the callback whether or not the trace is sampled. Errors it throws are logged without changing the response, and enqueued means the queue accepted the activity, not that a worker later processed it. Mock federations in @fedify/testing accept the same callback. See Observing inbox requests; the implementation is in #1201.
Actor dispatchers have been able to return a Tombstone since 2.2.0, but object dispatchers could only return null. A deleted post was answered with 404 Not Found, as if it had never existed, so applications that wanted 410 Gone, as ActivityPub recommends, needed their own route in front of Fedify.
Object dispatchers may now return a Tombstone, which Fedify serves with 410 Gone and the serialized tombstone after applying the authorization predicate:
federation.setObjectDispatcher(
Note,
"/users/{identifier}/notes/{id}",
async (ctx, values) => {
const post = await getPost(values.identifier, values.id);
if (post == null) return null;
if (post.deletedAt != null) {
return new Tombstone({
id: ctx.getObjectUri(Note, values),
formerType: Note,
deleted: post.deletedAt,
});
}
return new Note({ id: ctx.getObjectUri(Note, values), content: post.content });
},
);
The return type of ObjectDispatcher widens to TObject | Tombstone | null. RequestContext.getObject() still returns null for a tombstone unless you pass { tombstone: "passthrough" } or ask for the Tombstone or Object class itself. Dispatchers registered for Tombstone or Object that already returned tombstones now respond with 410 rather than 200. See Deleted objects; the change was proposed in #1112 and made in #1117.
Much of what Fedify fetches is chosen by the sender of an incoming request. Key IDs in signatures are one example, and the servers behind them can be slow on purpose. Several changes in 2.4.0 bound that work.
As #1130 points out, a request may carry several RFC 9421 signatures, and each one can make Fedify fetch a key from a URL of the sender's choosing. Fedify now verifies at most the first three, in Signature-Input order, and ignores the rest. A signature counts toward the limit even if it fails before its key is fetched, and a key named by several signatures is looked up once. The new FederationOptions.maxHttpSignatures option, added with the limit in #1166, changes it.
The built-in document loaders now give each call ten seconds by default, a limit proposed in #1131 and added in #1169. The time limit covers every redirect, alternate document, double-knocking retry, and the response body. A timed-out call throws a FetchError whose cause is a DOMException named "TimeoutError". Error responses are read in full before the loader throws, up to 1 MiB, so that a server cannot send a 404 header and then stall the body. FederationOptions.documentLoaderTimeout changes the limit or turns it off with null, and custom document loaders are not affected. Since #913, getDocumentLoader() also rejects HTML responses that do not advertise an ActivityPub alternate with a FetchError, rather than failing inside JSON.parse(), which #912 found cluttering error trackers.
Cached public keys and remembered HTTP Message Signatures specs used to live forever in the key–value store. #1017 reports a production Redis instance holding 194,424 public-key entries, about 165 MiB, for actors that had mostly stopped federating. Keys now expire after 30 days and specs after 90, configurable through FederationOptions.publicKeyTtl and FederationOptions.httpMessageSignaturesSpecTtl. Entries written by earlier versions have no expiry until they are next written. The Clearing legacy cache entries section shows how to remove them for Redis, PostgreSQL, and other stores. Heewon Chae (@heeoneie) implemented this in #1027, their first contribution to Fedify.
const federation = createFederation<void>({
kv,
maxHttpSignatures: 1, // default: 3
documentLoaderTimeout: { seconds: 5 }, // default: 10 seconds
publicKeyTtl: { days: 7 }, // default: 30 days
httpMessageSignaturesSpecTtl: { days: 30 }, // default: 90 days
});
Finally, verifyProof() now authenticates every option of an Ed25519 JCS proof except proofValue, including expires, domain, challenge, and nonce. It also rejects expired or malformed ones. Previously, a field such as expires could be added to an already signed proof without invalidating it. Jiwon Kwon (@z9mb1) spotted this in review of #968. Callers can now require a domain and challenge through VerifyProofOptions to prevent cross-domain or replayed use.
@fedify/adonisjs@fedify/adonisjs integrates Fedify with AdonisJS. Running node ace add @fedify/adonisjs registers a service provider that owns the federation's lifecycle and a server middleware that mounts it. It also scaffolds config/fedify.ts, start/federation.ts, and app/federation/main.ts. Controllers get a ctx.federation request context:
// app/federation/main.ts
import federation from "@fedify/adonisjs/services/builder";
import { Person } from "@fedify/vocab";
federation.setActorDispatcher("/actors/{identifier}", (ctx, identifier) =>
new Person({ id: ctx.getActorUri(identifier), preferredUsername: identifier }));
The middleware sits first in AdonisJS's server stack rather than its router stack. That way Fedify sees paths the router does not know, such as /.well-known/webfinger, and reads request bodies before the body parser consumes them. The package targets Node.js and is published to npm only. See the AdonisJS section of the integration guide.
Samuel Brinkmann (@sabrinkmann) contributed the package in #1006, closing #139, and built it on a prototype that Emelia Smith (@thisismissem) attached to that issue.
npm add @fedify/adonisjs
pnpm add @fedify/adonisjs
yarn add @fedify/adonisjs
@fedify/interaction-controlsGoToSocial interaction controls, FEP-044f quote authorization, and FEP-7aa9 featured collections share a request-and-authorization flow. A server asks to reply to, quote, or feature a post, and the author's server approves according to the post's interactionPolicy. @fedify/interaction-controls implements that flow for like, reply, announce, quote, and feature interactions. Each has a helper, such as likeInteraction or quoteInteraction, that creates and verifies requests and authorizations, evaluates policies, and recognizes interactions that arrived without a request:
import { likeInteraction } from "@fedify/interaction-controls";
// When a LikeRequest arrives:
const verified = await likeInteraction.verifyRequest(ctx, { request });
if (verified.verified) {
const decision = await likeInteraction.evaluatePolicy(ctx, {
subject: note,
requester: request.actorId!,
});
if (decision.result === "automatic") { // or "manual" or "denied"
const authorization = likeInteraction.createAuthorization({
id: new URL("https://example.com/authorizations/1"),
attributedTo: owner,
interactingObject: like,
interactionTarget: note,
});
// ...
}
}
The package does not install inbox listeners or store anything; the application decides where requests and authorizations live and when a human needs to approve. Verification fails closed. Without a policy, likes, replies, and announces are approved automatically, while quotes and features are denied. @fedify/vocab gains the FEP-7aa9 types this needs: FeaturedCollection, FeaturedItem, FeatureRequest, and FeatureAuthorization, along with actors' featuredCollections and InteractionPolicy.canFeature. Samuel Brinkmann asked for them in #810 once Mastodon 4.6 shipped the FEP. See the Interaction controls chapter; the work is in #914 and #929.
npm add @fedify/interaction-controls
pnpm add @fedify/interaction-controls
yarn add @fedify/interaction-controls
deno add jsr:@fedify/interaction-controls
bun add @fedify/interaction-controls
@fedify/netlify@fedify/netlify lets a Fedify application run on Netlify Functions without an external database or queue. NetlifyMessageQueue submits jobs as Netlify Async Workloads events, and createNetlifyQueueHandler() turns a Function into their consumer, with delayed delivery, native retries, and per-key FIFO ordering. NetlifyBlobsKvStore is a key–value store on Netlify Blobs with expiration, prefix listing, and compare-and-swap:
import {
createNetlifyQueueHandler,
NetlifyBlobsKvStore,
NetlifyMessageQueue,
} from "@fedify/netlify";
import { AsyncWorkloadsClient } from "@netlify/async-workloads";
import { getStore } from "@netlify/blobs";
export const kv = new NetlifyBlobsKvStore(
getStore({ name: "fedify", consistency: "strong" }),
);
export const queue = new NetlifyMessageQueue({
client: new AsyncWorkloadsClient(),
orderingKv: kv,
});
// netlify/functions/fedify-queue.ts
export default createNetlifyQueueHandler({
queue,
maxRetries: 4,
federation: () => builder.build({ kv, queue, manuallyStartQueue: true }),
});
The key–value store exists because of cost. #1010 describes a static blog whose ActivityPub traffic kept a Netlify Database from ever idling, which used most of the account's monthly credits. Jiwon Kwon contributed NetlifyBlobsKvStore in #1029, and the queue landed in #934. See Netlify Functions in the deployment guide.
npm add @fedify/netlify
pnpm add @fedify/netlify
yarn add @fedify/netlify
deno add jsr:@fedify/netlify
bun add @fedify/netlify
@fedify/pglite@fedify/pglite provides PgliteKvStore, a KvStore on an embedded PGlite database that you create and pass in. It uses the same schema as PostgresKvStore. It is meant for a single PGlite instance in a single runtime isolate, so it does not include a message queue: PGlite does not share data between processes.
import { PGlite } from "@electric-sql/pglite";
import { PgliteKvStore } from "@fedify/pglite";
const federation = createFederation({
kv: new PgliteKvStore(new PGlite("./data/fedify")),
});
ChanHaeng Lee contributed the package in #1020, together with testKvStore() in @fedify/testing. testKvStore() is a conformance suite for KvStore implementations, the counterpart to testMessageQueue(). See PgliteKvStore.
npm add @fedify/pglite
pnpm add @fedify/pglite
yarn add @fedify/pglite
deno add jsr:@fedify/pglite
bun add @fedify/pglite
Portable inbox forwarding, task deduplication, Netlify queue ordering, and the delivery circuit breaker all need an atomic compare-and-swap on the key–value store. PostgresKvStore now implements cas(), added in #934, and so does RedisKvStore, requested in #1163 and added in #1167. The Redis version uses a single-key Lua script, which works on standalone Redis and Redis Cluster. Before this, the circuit breaker fell back to racy updates on Redis.
Both come with conditions. RedisKvStore needs Redis to permit EVAL, and custom codecs must encode equal values identically. PostgresKvStore now creates logged tables by default, because losing sequence state in crash recovery could block queued events for good. It migrates existing unlogged tables when it initializes. That one-time migration rewrites the table and locks it exclusively, so schedule the upgrade accordingly if the table is large or busy; unlogged: true keeps the old behavior.
fedify init and the CLIfedify init can now scaffold a SvelteKit project, contributed by Jang Hanarae (@menele) in #971. Jang Hanarae also added a test task to generated projects in #990. It starts the app, waits for it to be ready, and checks that it resolves a local actor, which gives new projects a smoke test to run after scaffolding and after later changes. Before generating anything, fedify init now checks that the selected Deno, Bun, or Node.js meets Fedify's minimum version, or a framework's higher one, such as Astro's Node.js 22.12. It fails with a clear error in non-interactive mode, and in interactive mode it disables the affected package managers. Lee Jeongmin (@userjmmm) contributed the check in #981. Astro projects now use Astro 7, and since #936 @fedify/astro is tested against Astro 5, 6, and 7 with real builds on Node.js, Deno, and Bun, as #931 asked.
fedify tunnel gains fedify.com.es, a tunneling service run by the Fedify project that needs no account; the CLI pins its SSH host key and refuses a mismatched server before exposing a local port. localhost.run is removed in the same change, #940, because the service no longer exists. fedify lookup now times out each request after ten seconds when -T/--timeout is not given, and the option now also bounds the wait for a DNS lookup. The CLI moves to Optique 1.3.2 in #1197, which handles attached values like --timeout=30 consistently, gives better suggestions for mistyped options, and no longer hides errors for known options behind positional arguments before --.
@fedify/lint gains several rules beyond the media upload ones mentioned above. actor-preferred-username-required, requested in #895 and added in #1022, warns when an actor dispatcher returns an actor without preferredUsername. outbox-listener-delivery-not-awaited, from #1057 and #1067, reports an outbox listener that calls ctx.sendActivity() or ctx.forwardActivity() and drops the promise. On Cloudflare Workers, such an activity may never leave, because pending work is discarded once the response returns. await, return, Promise.all(), Promise.allSettled(), and waitUntil() count as handling the promise, while void, Promise.race(), and Promise.any() are accepted as a deliberate choice not to wait. The rule is a warning in the ESLint recommended configuration and an error in strict. Oxlint users enable it by name, and it is not available in Deno Lint.
In #1050, which resolves #900, the existing outbox-listener-delivery-required rule began following control flow instead of scanning the listener's source as text. It reports a listener whose only delivery calls sit in a dead branch, after an unconditional return or throw, or in a function that is never used, and it stays quiet when it cannot tell. Jae-Hyuk-Jang (@jhyuk) contributed this change and both new rules. archie (@ArchieTansaria) then taught both delivery rules about statically false loops and loop binding patterns in #1088. See the linting guide.
@fedify/vocab supports the FEP-22cd draft for translations. A Translation records a translated version's language, translators, source object, and source review time, and the new Object.translations property attaches them to a post without creating separate posts. See Translation metadata. (#1037, #1038)@fedify/vocab-tools gains an extraContext property option, which adds a JSON-LD context only when its terms are used, and a trustEmbeddedObjects type option. (#1037, #1038)suppressError: true handles are now logged as warnings rather than errors, so that they no longer show up as application errors. Contributed by Jae Hui Hong (@axz0612). (#933, #1035)enqueueMany() makes separate sends can set MessageQueue.atomicEnqueueMany to false. Fedify then rejects a multi-message batch governed by one deduplicationKey up front, since a partial send followed by a retry could enqueue duplicates. (#930, #934)Temporal bundle temporal-polyfill instead of requiring @js-temporal/polyfill at runtime, and type declarations rely on the standard esnext.temporal lib. (#823, #925)Accept header now includes the ActivityStreams profile on the JSON-LD media type, as ActivityPub and FEP-ef61 gateways require. (#834, #1077)fedify lookup --recurse reporting a timeout or other network failure as a possibly private object; it now reports the actual cause. (#1156, #1199)-p/--allow-private-address option of fedify webfinger being ignored. (#1156, #1199)outbox-listener-delivery-required and outbox-listener-delivery-not-awaited losing track of an object's functions when a nested helper assigns another property of that object. (#1125, #1140)npm add @fedify/fedify@2.4.0
pnpm add @fedify/fedify@2.4.0
yarn add @fedify/fedify@2.4.0
deno add jsr:@fedify/fedify@2.4.0
bun add @fedify/fedify@2.4.0
Update the other @fedify/* packages you use to 2.4.0 at the same time. To update the CLI, run npm install -g @fedify/cli@2.4.0, or see the installation instructions for other package managers.
Most applications need no code changes, but these changes in behavior may affect you:
401 Unauthorized unless they carry a valid Object Integrity Proof by the actor's DID. Keys at compatible identifiers, and keys at ordinary URLs that claim a portable owner, are no longer trusted by origin.Context as their options, such as await create.getObject(ctx), now verify the portable objects they dereference, including references to compatible identifiers from objects with ordinary HTTP(S) IDs./.well-known/apgateway/, and Context.sendActivity() may reject an activity that embeds others' portable objects. Next.js applications need the matcher shown above to serve hashlink media.maxHttpSignatures if you need more.fedify lookup does too.Tombstone or Object that return tombstones now respond with 410 Gone instead of 200 OK.PostgresKvStore migrates existing unlogged tables to logged tables on initialization, which locks the table.RedisKvStore needs EVAL permission for cas().Context and RequestContext interfaces need the new getPortable*Uri() methods and isSignedByAudience().fedify tunnel no longer supports localhost.run.Thanks to this release's contributors:
@fedify/adonisjs, and the request for FEP-7aa9 vocabularytest task in fedify initfedify initactor-preferred-username-required and outbox-listener-delivery-not-awaited lint rules, and control-flow analysis for outbox-listener-delivery-requiredChanHaeng Lee (@2chanhaeng) and Jiwon Kwon (@z9mb1) joined as maintainers after 2.3.0. ChanHaeng built the background task API and @fedify/pglite, Jiwon built NetlifyBlobsKvStore and rewrote the relay documentation, and both reviewed much of the rest of this release.
Many people added tests, documentation, and infrastructure during this cycle, often starting from a good first issue: Seoa Yoon (@bananamilk452), Lim Kyoujin (@dktsudgg), @m2nsp, Cho Seongmin (@seongmin36), Safal Shrestha (@Safal-Shrestha-SS), Jo Youngjae (@jaonz6057), @kecan0406, Loi Nguyen (@lntutor), Oh Taejun (@0w0), Yerin Park (@dstforye), Lee Dogeon (@moreal), and Sae Jin Kim (@siliconsjang).
The FEP-ef61 work owes a lot to people outside the project. @anotherdoesnm opened the request in #288. @silverpill, the author of FEP-ef61 and FEP-ae97, clarified the gateway trust model, reviewed the compound proof profile, and checked its test vectors. @dmitrizagidulin and @by_caballero weighed in on the profile's JSON-LD questions in #938. Testing against tootik and Mitra led to two of the interoperability fixes in this release.
See CHANGES.md for the complete changelog.
github.com
ActivityPub server framework in TypeScript. Contribute to fedify-dev/fedify development by creating an account on GitHub.
Of all the ActivityPub servers that have ever been publicly available, how many do you think are still up today?
Of all the ActivityPub servers that have ever been publicly available, how many do you think are still up today?
There's a new very lightweight Fediverse platform that's just been released called littleFedi. You can follow its new official account at:
Its official website including download and source code is at:
LittleFedi is for techy people who want to create their own Fediverse server using as few resources as possible.
It's written by a group called "Little Computer People", its maintainer is @stefano
littlefedi.org
A small ActivityPub server written in Go. One binary, no runtime, no external services.
There's a new very lightweight Fediverse platform that's just been released called littleFedi. You can follow its new official account at:
Its official website including download and source code is at:
LittleFedi is for techy people who want to create their own Fediverse server using as few resources as possible.
It's written by a group called "Little Computer People", its maintainer is @stefano
littlefedi.org
A small ActivityPub server written in Go. One binary, no runtime, no external services.
LittleFedi is an excellent light weight but fully featured Fediverse platform (ActivityPub). I've been kicking tires and providing feedback on the test instance and excited for the first release to give it a go self-hosted.
@stefano has been leading a master class in community and developer relations. He has been stewarding the team and user community with enthusiasm and transparency. Congratulations to Stefano and the team.
Here's a great story of the project's genesis and journey. https://littleone.littlefedi.social/@littlefedi_official/01a0f644-8ed6-7a7d-ade6-46f4c0a74f41
littleone.littlefedi.social
There's a new very lightweight Fediverse platform that's just been released called littleFedi. You can follow its new official account at:
Its official website including download and source code is at:
LittleFedi is for techy people who want to create their own Fediverse server using as few resources as possible.
It's written by a group called "Little Computer People", its maintainer is @stefano
littlefedi.org
A small ActivityPub server written in Go. One binary, no runtime, no external services.
There's a new very lightweight Fediverse platform that's just been released called littleFedi. You can follow its new official account at:
Its official website including download and source code is at:
LittleFedi is for techy people who want to create their own Fediverse server using as few resources as possible.
It's written by a group called "Little Computer People", its maintainer is @stefano
littlefedi.org
A small ActivityPub server written in Go. One binary, no runtime, no external services.
LittleFedi is an excellent light weight but fully featured Fediverse platform (ActivityPub). I've been kicking tires and providing feedback on the test instance and excited for the first release to give it a go self-hosted.
@stefano has been leading a master class in community and developer relations. He has been stewarding the team and user community with enthusiasm and transparency. Congratulations to Stefano and the team.
Here's a great story of the project's genesis and journey. https://littleone.littlefedi.social/@littlefedi_official/01a0f644-8ed6-7a7d-ade6-46f4c0a74f41
littleone.littlefedi.social
Why is #ActivityPub missing from the Local First Conference?
Why is #ActivityPub missing from the Local First Conference?
is there an exhaustive list of client-2-server apis for #ActivityPub ?
is there an exhaustive list of client-2-server apis for #ActivityPub ?
Fedify 2.4.0 is out.
The main addition is support for FEP-ef61 portable objects. A Fedify server can now host actors whose identity is a DID instead of a domain, serve their signed objects through /.well-known/apgateway/, and verify the portable objects of other servers. I tested it against tootik and Mitra, and wrote up where Fedify's profile differs from the FEP and what it leaves out.
The release also adds application-defined background tasks, the ActivityPub Media Upload extension, per-request inbox reports, 410 Gone for deleted objects, and four new packages (AdonisJS, Netlify, PGlite, and interaction controls).
Some defaults changed, so please read the upgrading section before you update, especially if you federate with software that uses FEP-ef61 compatible identifiers.
Release notes: https://hackers.pub/@fedify/2026/fedify-2-4.
Thanks to everyone who contributed to this cycle, and to @silverpill for patient answers about the FEP.
hackers.pub
Fedify is a TypeScript framework for building ActivityPub servers. It implements federation details such as HTTP Signatures, JSON-LD processing, WebFinger, inbox and outbox routing, and activity delivery.
Fedify 2.4.0 is a minor release with an unusually long changelog. The main addition is support for FEP-ef61 portable objects: a Fedify server can now host actors whose identity is a DID rather than a domain, serve and verify their signed objects, and consume the portable objects of other servers. The release also adds an API for application-defined background tasks, the ActivityPub Media Upload extension, a hook that reports how every inbox request was authenticated and handled, and 410 Gone tombstones for deleted objects. Several defaults change to limit what a hostile peer can make Fedify do. Four new packages ship with this version: @fedify/adonisjs, @fedify/interaction-controls, @fedify/netlify, and @fedify/pglite.
Most applications can upgrade without code changes, but a few of the new defaults are behavior changes. They are listed in the Upgrading section at the end.
An ActivityPub object's ID is an HTTP(S) URL on its server's domain. If that server shuts down, its IDs stop resolving, and an actor that moves elsewhere gets a new ID that everyone has to follow again. FEP-ef61, written by @silverpill of Mitra, proposes IDs that do not name a server at all. A portable ID is an ap: URI whose authority is a DID:
ap://did:key:z6MkrJVnaZkeFzdQyMZu1cgjg7k1pZZ6pvBQ7XJPt4swbTQ2/actor
The same object can be stored on several servers, called gateways, each of which serves it under /.well-known/apgateway/. Because no server owns the ID, a portable actor, activity, or object is authenticated only by its FEP-8b32 Object Integrity Proof, made with a key of the DID in its ID. A portable actor lists its gateways in a gateways property, and other servers deliver to its inbox through them.
Fedify 2.4.0 implements a profile of FEP-ef61 for gateways and consumers of portable objects. It makes two deliberate choices where the FEP or the specifications it rests on leave questions open, and it leaves out a few parts of the proposal; both are described below. The new Portable objects chapter of the manual walks through running portable actors from start to finish and then describes the profile as a whole, so that other implementers can tell what to expect from a Fedify server. The request came from @anotherdoesnm in #288, which tracked the work from a roadmap to the final pieces.
Portable objects are a foundation for nomadic identity, not nomadic identity itself. Fedify serves and verifies portable objects and forwards activities between gateways, but it does not copy actors or posts between servers, rotate keys, or move an actor to another DID. Full nomadic identity is still open in #413.
A portable actor starts with an Ed25519 key pair. Its DID is made from the public key with exportDidKey(), and whoever holds the private key can act as the actor, so store it with the actor and keep it safe:
import { generateCryptoKeyPair } from "@fedify/fedify";
import { exportDidKey } from "@fedify/vocab-runtime";
const { publicKey, privateKey } = await generateCryptoKeyPair("Ed25519");
const did = await exportDidKey(publicKey); // did:key:z6Mk...
// The verification method that the actor's proofs refer to:
const didKeyId = new URL(`${did}#${did.slice("did:key:".length)}`);
The actor dispatcher returns a portable actor as it would an ordinary one. Context.getPortableActorUri() and its siblings for inboxes, outboxes, collections, and objects build portable IDs from the dispatcher's paths and the DID, and signObject() signs the actor with the DID's key:
federation
.setActorDispatcher("/users/{identifier}", async (ctx, identifier) => {
const user = await findUser(identifier);
if (user == null) return null;
// A request through the gateway endpoint names the DID in its path,
// e.g., GET /.well-known/apgateway/did:key:z6Mk.../users/alice:
if (
ctx.portableRequest != null &&
ctx.portableRequest.authority !== user.did
) {
return null;
}
// This server's gateway keys for the actor (see below):
const keys = await ctx.getActorKeyPairs(identifier);
return await signObject(
new Person({
id: ctx.getPortableActorUri(identifier, user.did), // ap+ef61://did:key:z6Mk.../users/alice
preferredUsername: user.username,
inbox: ctx.getPortableInboxUri(identifier, user.did),
outbox: ctx.getPortableOutboxUri(identifier, user.did),
followers: ctx.getPortableFollowersUri(identifier, user.did),
gateways: [new URL("https://example.com")],
publicKeys: keys.map((key) => key.cryptographicKey),
assertionMethods: keys.map((key) => key.multikey),
}),
user.didKeyPair.privateKey,
new URL(`${user.did}#${user.did.slice("did:key:".length)}`),
);
})
.setKeyPairsDispatcher(async (ctx, identifier) => {
const user = await findUser(identifier);
// This server's own key pair for the actor, not the DID's:
return user == null ? [] : [user.gatewayKeyPair];
})
.mapPortableActorId(async (ctx, identifier) => {
const user = await findUser(identifier);
return user == null ? null : ctx.getPortableActorUri(identifier, user.did);
});
With this dispatcher, Fedify serves the actor at its compatible identifier, https://example.com/.well-known/apgateway/did:key:z6Mk.../users/alice, which is how other servers retrieve it. It serves it through WebFinger as @alice@example.com, since the handle's domain comes from the first gateway in gateways, and at its ordinary URL as before. On the gateway endpoint, Fedify serves a document only if its ID is the requested portable ID and it carries a proof made with a key of that DID.
The key pairs dispatcher and the new ActorCallbackSetters.mapPortableActorId() give the actor gateway keys. These are the server's own keys for the actor, and they sign only the HTTP requests the server makes on the actor's behalf, such as deliveries. They never sign the actor's activities or objects; only the DID's key does that. The actor document lists them under assertionMethods, as FEP-521a describes, so that other servers can verify the HTTP Signatures. See Dispatching the actor and Gateway keys of portable actors for the details.
Object and collection dispatchers serve portable objects and collections through the gateway endpoint in the same way, routed by the path after the DID. Two rules protect the actor's data. A non-public portable object is served only if the dispatcher has an authorization predicate, because FEP-ef61 forbids a gateway to show it to anyone outside its audience; the new RequestContext.isSignedByAudience() helps to write one. Without a predicate, a request for such an object gets 404 Not Found, as though the gateway did not store it. A deleted object is served as a signed Tombstone with 410 Gone. Resources that portable objects refer to by SHA-256 hashlinks, such as GET /.well-known/apgateway/hl:zQm..., are served by the new Federatable.setHashlinkMediaDispatcher().
A portable actor's activities have to be signed by its DID, which Fedify's key pairs dispatcher does not know about. Sign them with signObject() before sending, and pass normalizeExistingProofs: true so that the activity goes out in the form its proof covers. An activity that embeds a portable object, such as the Note of a Create, is signed inside out:
const actor = withGatewayHints(
ctx.getPortableActorUri(user.identifier, user.did),
[new URL("https://example.com")],
); // ap+ef61://did:key:z6Mk.../users/alice?@gateway=https%3A%2F%2Fexample.com
const note = await signObject(
new Note({
id: ctx.getPortableObjectUri(
Note,
{ identifier: user.identifier, id: crypto.randomUUID() },
user.did,
),
attribution: actor,
to: PUBLIC_COLLECTION,
content: "Hello, world!",
}),
user.didKeyPair.privateKey,
didKeyId,
);
const create = await signObject(
new Create({
id: ctx.getPortableObjectUri(
Note,
{ identifier: user.identifier, id: crypto.randomUUID() },
user.did,
),
actor,
to: PUBLIC_COLLECTION,
object: note,
}),
user.didKeyPair.privateKey,
didKeyId,
);
await ctx.sendActivity(
{ identifier: user.identifier },
"followers",
create,
{ normalizeExistingProofs: true },
);
This example depends on a fix to signObject() that is useful outside FEP-ef61 as well. Previously, a signed object assigned to a typed parent was rebuilt under the parent's JSON-LD context when the parent was serialized. The child kept its proofValue but lost the document its proof covered, so it no longer verified on its own. signObject() now captures the exact document it signed, and nested serialization embeds that document verbatim. The captured document is a snapshot: clone() does not carry it, so sign a clone again if it has to be embedded. The design is in #1044 and the fix in #1051.
Context.sendActivity() refuses a portable activity that is unsigned, signed by the wrong key, or carries a Linked Data Signature, and throws a TypeError before anything is queued. It also refuses a few shapes of compound document that Fedify's own inboxes would reject, such as a received signed Follow embedded in an Accept; refer to such an object by its ID instead. Deliveries to portable inboxes go through the recipient's gateways, or else the @gateway hints on the inbox, trying at most five of them until one accepts. Before this release, they failed with UrlError: Unsupported protocol: ap+ef61:. The rules are in Delivering to portable actors and Producing a compound document.
On the receiving side, a portable inbox is reached as POST /.well-known/apgateway/did:key:z6Mk.../users/alice/inbox and dispatched to the same inbox listeners as the ordinary inbox. Context.parseUri() gains a portable option, and the parsed result has the DID in a new authority property to check against the one you store. Fedify accepts an activity of a portable actor, or one with a portable ID, only if it has a valid proof by that DID; neither an HTTP Signature nor a Linked Data Signature authenticates it. Each portable object embedded in the activity is verified against its own proof before dispatch. An accepted activity that carries its own proof is forwarded to the actor's other gateways at most once, as FEP-ef61 recommends. Forwarding needs a key–value store that supports cas(), and the FederationOptions.portableInboxForwarding option tunes it. See Portable inboxes and Forwarding to other gateways.
Applications that never host a portable actor still meet portable objects from other servers. Context.lookupObject() resolves portable IDs, compatible identifiers, and the fediverse handles of portable actors, and returns portable objects only if they verify:
const actor = await ctx.lookupObject("@alice@example.com");
// A portable ID without @gateway hints needs gateways to ask:
const note = await ctx.lookupObject("ap://did:key:z6Mk.../notes/1", {
gateways: ["https://example.com"],
});
Property accessors fetch portable references through gateways as well, and they verify what they fetch with the new verifyPortableObject option. Context has a matching verifyPortableObject property, so passing a context as the options, as in await create.getObject(ctx), is enough. Activities received in inboxes use this verifier by default, and objects fetched through their accessors inherit it, so a chained call such as (await create.getObject(ctx))?.getAttribution() verifies each portable object along the way. A lookup asks at most five gateways, falling back to the next when one fails or serves something that does not verify. Unsigned collections are accepted only from a gateway that the owner lists, and only for an actor's inbox, outbox, followers, following, and liked, as FEP-ef61 allows. fetchPortableMedia() retrieves the media of a portable object and returns it only if it matches the object's digestMultibase. See Dereferencing portable references and Default verifiers.
Software that does not understand ap: URIs can refer to a portable object by a compatible identifier, an HTTP(S) URL under a gateway's /.well-known/apgateway/ path such as https://gw.example/.well-known/apgateway/did:key:z6Mk.../actor. Some implementations, tootik among them, use compatible identifiers as the IDs of their portable actors.
Earlier versions of Fedify treated these URLs as ordinary web URLs and trusted them by the server that served them. Since anyone can serve a compatible identifier for any DID, https://evil.example/.well-known/apgateway/did:key:z6MkAlice/actor could act as Alice without a proof by Alice's DID. Fedify 2.4.0 treats a compatible identifier as the portable object it stands for, as FEP-ef61 requires. Inboxes reject an activity from an actor whose ID is a compatible identifier with 401 Unauthorized unless it has a valid Object Integrity Proof by the actor's DID, and there is no option to turn this off. Actors that publish compatible identifiers without signing them are no longer accepted, and neither are those whose DIDs use key types that Fedify cannot verify yet. The change was tracked in #1093 and made in #1105.
Your own portable actors may still use compatible identifiers as their IDs, for software that rejects ap: IDs. Build them with toCompatibleEf61Id() before signing, and Fedify will treat them as portable wherever it serves or sends them. It also warns if one is malformed or does not point at the actor's first gateway. See Compatible identifiers as actor IDs.
Two choices in Fedify's profile differ from the current FEP text or rest on semantics that are not settled yet. Fedify keeps both for the 2.x series, and either may change in Fedify 3.0.
The first is the scheme. FEP-ef61 recommends ap: and canonicalizes ap+ef61: to it, while also warning that the recommendation may change to ap+ef61:, since these IDs are meant only for portable objects. Fedify accepts both, compares them as equal, and serializes ap+ef61:. Parsing a document normalizes its portable IRIs to ap+ef61:, so re-serializing an object that another implementation signed with ap: IDs breaks its proof. Keep the received JSON of such documents, and compare portable IDs with arePortableUrisEqual() rather than as strings. The discussion is in #826 and #828, and the reasoning is in Canonical ap+ef61: scheme.
The second concerns compound documents. When a signed Note sits inside a signed Create, the verifier has to know which part of the JSON each proof covers, and neither FEP-8b32 nor Verifiable Credential Data Integrity defines that yet; the question is open in w3c/vc-data-integrity#350. Fedify uses an interim map-local profile, worked out in #938. Each JSON map with a proof is its own secured document, each embedded portable object needs its own proof and its own @context, and proof sets and chains are not supported. The profile authenticates the exact JSON values each proof covers; it does not promise that a child's JSON-LD expansion inside its parent matches its expansion on its own. @silverpill, @dmitrizagidulin, and @by_caballero reviewed the proposal in that thread, and @silverpill reproduced its test vectors independently. See Map-local compound proofs.
Fedify does not implement FEP-ae97, the companion proposal for client-side signing. There are no endpoints for registering actors, submitting client-signed activities to a portable outbox, or gateway discovery, and no media upload and deletion endpoints under /.well-known/apgateway-media. Your own outbox listeners can still relay activities that a client signed, unchanged. Fedify also does not copy or reconcile actors, objects, or collections between gateways, so running an actor on several gateways means keeping their data in sync yourself. There is no workflow for rotating a DID's key or migrating an actor to another DID. Fedify resolves verification methods only for did:key DIDs with Ed25519 keys; other DID methods verify only if your document loader resolves them. Gateways must be plain origins, since the arbitrary gateway paths that FEP-ef61 discusses are not supported. The full list is in Not supported.
The profile was tested against tootik v0.25.4 and Mitra v5.10.0 in #1202. Fedify verifies tootik's portable actors, activities, and HTTP Signatures, and Mitra's gateway HTTP Signatures on activities that a client signed. Both accept the portable activities that Fedify sends. Two fixes came out of this testing. Fedify now reads a gateways list that tootik leaves unmapped in its JSON-LD context. It also serializes gateways as bare origins, because Mitra rejects a trailing slash. Fedify has not yet verified an actor or activity that Mitra signed itself, and Mastodon's handling of portable actors has not been tested. The FEP-ef61 interoperability section of FEDERATION.md records what was tested and how.
fedify lookup looks up portable objects by their ap: and ap+ef61: IDs, compatible identifiers, and the handles of portable actors, and it verifies their proofs. When no gateway returns an acceptable object, it says why. A new --gateway option on fedify lookup, fedify inbox, and fedify webfinger names the gateways to ask. fedify inbox --follow follows portable actors, and fedify webfinger accepts portable actor IDs. See Looking up portable objects in the CLI manual.
The mock federation and contexts in @fedify/testing support portable IDs, verification against fixture document loaders, gateway requests, and hashlink media, so tests can exercise portable objects without a live gateway. @fedify/lint's actor URI rules accept the portable IDs that the getPortable*Uri() methods build. @fedify/next now passes hashlink media requests to Fedify, a problem reported in #1149 and fixed in #1170; to have Next.js run the middleware for them, add { source: "/.well-known/apgateway/:path*" } to your matcher.
Fedify already runs activity delivery and inbox processing on queue workers, but applications usually have jobs of their own: digest emails, timeline rebuilds, link previews. Until now, those jobs could not use Fedify's queues, serialization, or retries. Fedify 2.4.0 opens the same machinery to application-defined tasks.
A task is defined once with defineTask() and enqueued from any Context with enqueueTask() or enqueueTaskMany(). Every task takes a Standard Schema, such as a Zod or Valibot schema, which types its payload. Fedify validates the payload when it is enqueued and again when a worker picks it up; the second check catches payloads from an earlier deployment that no longer match the schema:
import { createExponentialBackoffPolicy } from "@fedify/fedify";
import { z } from "zod";
const sendDigest = federation.defineTask("sendDigest", {
schema: z.object({ userId: z.string(), since: z.date() }),
handler: async (ctx, data) => {
// data is typed as { userId: string; since: Date }
await sendEmail(data.userId, await buildDigest(data.userId, data.since));
},
retryPolicy: createExponentialBackoffPolicy({ maxAttempts: 3 }),
});
// Anywhere with a Context:
await ctx.enqueueTask(sendDigest, { userId: "alice", since: new Date() }, {
delay: { minutes: 30 },
deduplicationKey: "digest:alice",
});
Fedify serializes payloads with devalue, so Date, Map, Set, URL, bigint, Temporal values, circular references, and Activity Vocabulary objects all survive the trip through any message queue backend. A Note in a payload comes back as a Note. Failed handlers are retried with exponential backoff by default, and queues with native retries handle retries themselves. Tasks fall back to the outbox queue unless you give them their own. Use the new task slot of the queue option or a per-task queue for that, and startQueue(ctxData, { queue: "task" }) runs a task-only worker. Setting taskQueueResolution: "strict" turns the fallback into an error.
A deduplicationKey deduplicates enqueues. The feature came from @julian's point in #206 that recurring tasks are hard to manage when you cannot tell whether one is already queued. A queue that declares nativeDeduplication enforces the key itself; otherwise, Fedify keeps a best-effort marker in the key–value store through cas(). Each task runs in a fedify.task span, and the queue metrics from 2.3.0 now include task queues. The Background tasks chapter covers routing, retries, deduplication, and the limitations; cron-style scheduling, result backends, and priorities are out of scope for now.
ChanHaeng Lee (@2chanhaeng) implemented the API: the core in #803, deduplication in #806, and observability in #812, following the plans in #797, #798, and #799. The work was merged in #923, closing #206, which had been open since February 2025.
Fedify 2.2.0 added client-to-server outbox listeners, but a C2S client also has to upload images and video before it can post them. That traffic does not go through POST /outbox. Fedify 2.4.0 supports the ActivityPub Media Upload extension, a SocialCG proposal last updated in 2017 that never reached W3C Recommendation status.
setMediaUploader() registers a multipart/form-data endpoint. Its callback receives the uploaded file and the object shell the client posted along with it. If the callback returns the created object, Fedify responds with 201 Created; if it returns a URL where the object will appear once processing finishes, Fedify responds with 202 Accepted:
federation
.setMediaUploader(
"/users/{identifier}/media",
async (ctx, identifier, file, object) => {
const stored = await uploadToStorage(file);
return new Image({
id: ctx.getObjectUri(Image, { uuid: stored.uuid }),
url: new URL(stored.publicUrl),
mediaType: file.type,
name: object.name,
});
},
)
.authorize(async (ctx, identifier) => {
const session = await verifyAccessToken(
ctx.request.headers.get("authorization"),
);
return session?.identifier === identifier;
});
The actor dispatcher advertises the endpoint itself by setting endpoints: new Endpoints({ uploadMedia: ctx.getMediaUploaderUri(identifier) }), the same way it places its inbox and outbox URIs. Without an authorize() hook, anyone who finds the endpoint can upload to it. For that reason, Fedify logs a warning when an uploader is registered without one, and when the returned object's URI does not match an object dispatcher or the actor does not advertise the endpoint. Four new @fedify/lint rules catch the same mistakes before they run. Fedify reads the upload into memory and sets no size limit of its own, so cap the request size at your reverse proxy. Storing the file and serving the object back are up to the application. See the Media upload chapter; the work is in #754 and #927.
Fedify decides several things about every inbox delivery: which signature mechanisms applied, which keys they tried, whether the actor owned the key, and what happened to the activity. Before this release, applications saw only part of that. A Linked Data Signature by a key the actor did not own was counted the same as a bad signature, and the key that verified a request was never handed to the application. The request in #1191 came from an effort to log these decisions for a debugging tool.
The new onRequestFinished() callback on inbox listeners receives an InboxRequestReport for every delivery to an inbox route, including rejected requests and requests that fail before verification:
federation
.setInboxListeners("/users/{identifier}/inbox", "/inbox")
.on(Follow, async (ctx, follow) => { /* ... */ })
.onRequestFinished(async (ctx, report) => {
await db.insertInboxLog({
inbox: report.inbox.kind, // "personal" | "shared" | "portable"
authentication: report.authentication.status,
outcome: report.outcome.type === "response"
? report.outcome.disposition // "processed" | "enqueued" | "duplicate" | ...
: `exception:${report.outcome.stage}`,
keyIds: report.attempts
.flatMap((attempt) => attempt.checks)
.flatMap((check) => check.triedKeys)
.map((key) => key.id?.href),
});
});
Each HTTP Signature, Linked Data Signature, or Object Integrity Proof check appears as an attempt with the actual keys it tried, including a stale cached key and its refreshed replacement. The report also carries the final authentication decision, such as actorKeyMismatch or proofPolicy, and the response or exception that ended the request. Fedify awaits the callback whether or not the trace is sampled. Errors it throws are logged without changing the response, and enqueued means the queue accepted the activity, not that a worker later processed it. Mock federations in @fedify/testing accept the same callback. See Observing inbox requests; the implementation is in #1201.
Actor dispatchers have been able to return a Tombstone since 2.2.0, but object dispatchers could only return null. A deleted post was answered with 404 Not Found, as if it had never existed, so applications that wanted 410 Gone, as ActivityPub recommends, needed their own route in front of Fedify.
Object dispatchers may now return a Tombstone, which Fedify serves with 410 Gone and the serialized tombstone after applying the authorization predicate:
federation.setObjectDispatcher(
Note,
"/users/{identifier}/notes/{id}",
async (ctx, values) => {
const post = await getPost(values.identifier, values.id);
if (post == null) return null;
if (post.deletedAt != null) {
return new Tombstone({
id: ctx.getObjectUri(Note, values),
formerType: Note,
deleted: post.deletedAt,
});
}
return new Note({ id: ctx.getObjectUri(Note, values), content: post.content });
},
);
The return type of ObjectDispatcher widens to TObject | Tombstone | null. RequestContext.getObject() still returns null for a tombstone unless you pass { tombstone: "passthrough" } or ask for the Tombstone or Object class itself. Dispatchers registered for Tombstone or Object that already returned tombstones now respond with 410 rather than 200. See Deleted objects; the change was proposed in #1112 and made in #1117.
Much of what Fedify fetches is chosen by the sender of an incoming request. Key IDs in signatures are one example, and the servers behind them can be slow on purpose. Several changes in 2.4.0 bound that work.
As #1130 points out, a request may carry several RFC 9421 signatures, and each one can make Fedify fetch a key from a URL of the sender's choosing. Fedify now verifies at most the first three, in Signature-Input order, and ignores the rest. A signature counts toward the limit even if it fails before its key is fetched, and a key named by several signatures is looked up once. The new FederationOptions.maxHttpSignatures option, added with the limit in #1166, changes it.
The built-in document loaders now give each call ten seconds by default, a limit proposed in #1131 and added in #1169. The time limit covers every redirect, alternate document, double-knocking retry, and the response body. A timed-out call throws a FetchError whose cause is a DOMException named "TimeoutError". Error responses are read in full before the loader throws, up to 1 MiB, so that a server cannot send a 404 header and then stall the body. FederationOptions.documentLoaderTimeout changes the limit or turns it off with null, and custom document loaders are not affected. Since #913, getDocumentLoader() also rejects HTML responses that do not advertise an ActivityPub alternate with a FetchError, rather than failing inside JSON.parse(), which #912 found cluttering error trackers.
Cached public keys and remembered HTTP Message Signatures specs used to live forever in the key–value store. #1017 reports a production Redis instance holding 194,424 public-key entries, about 165 MiB, for actors that had mostly stopped federating. Keys now expire after 30 days and specs after 90, configurable through FederationOptions.publicKeyTtl and FederationOptions.httpMessageSignaturesSpecTtl. Entries written by earlier versions have no expiry until they are next written. The Clearing legacy cache entries section shows how to remove them for Redis, PostgreSQL, and other stores. Heewon Chae (@heeoneie) implemented this in #1027, their first contribution to Fedify.
const federation = createFederation<void>({
kv,
maxHttpSignatures: 1, // default: 3
documentLoaderTimeout: { seconds: 5 }, // default: 10 seconds
publicKeyTtl: { days: 7 }, // default: 30 days
httpMessageSignaturesSpecTtl: { days: 30 }, // default: 90 days
});
Finally, verifyProof() now authenticates every option of an Ed25519 JCS proof except proofValue, including expires, domain, challenge, and nonce. It also rejects expired or malformed ones. Previously, a field such as expires could be added to an already signed proof without invalidating it. Jiwon Kwon (@z9mb1) spotted this in review of #968. Callers can now require a domain and challenge through VerifyProofOptions to prevent cross-domain or replayed use.
@fedify/adonisjs@fedify/adonisjs integrates Fedify with AdonisJS. Running node ace add @fedify/adonisjs registers a service provider that owns the federation's lifecycle and a server middleware that mounts it. It also scaffolds config/fedify.ts, start/federation.ts, and app/federation/main.ts. Controllers get a ctx.federation request context:
// app/federation/main.ts
import federation from "@fedify/adonisjs/services/builder";
import { Person } from "@fedify/vocab";
federation.setActorDispatcher("/actors/{identifier}", (ctx, identifier) =>
new Person({ id: ctx.getActorUri(identifier), preferredUsername: identifier }));
The middleware sits first in AdonisJS's server stack rather than its router stack. That way Fedify sees paths the router does not know, such as /.well-known/webfinger, and reads request bodies before the body parser consumes them. The package targets Node.js and is published to npm only. See the AdonisJS section of the integration guide.
Samuel Brinkmann (@sabrinkmann) contributed the package in #1006, closing #139, and built it on a prototype that Emelia Smith (@thisismissem) attached to that issue.
npm add @fedify/adonisjs
pnpm add @fedify/adonisjs
yarn add @fedify/adonisjs
@fedify/interaction-controlsGoToSocial interaction controls, FEP-044f quote authorization, and FEP-7aa9 featured collections share a request-and-authorization flow. A server asks to reply to, quote, or feature a post, and the author's server approves according to the post's interactionPolicy. @fedify/interaction-controls implements that flow for like, reply, announce, quote, and feature interactions. Each has a helper, such as likeInteraction or quoteInteraction, that creates and verifies requests and authorizations, evaluates policies, and recognizes interactions that arrived without a request:
import { likeInteraction } from "@fedify/interaction-controls";
// When a LikeRequest arrives:
const verified = await likeInteraction.verifyRequest(ctx, { request });
if (verified.verified) {
const decision = await likeInteraction.evaluatePolicy(ctx, {
subject: note,
requester: request.actorId!,
});
if (decision.result === "automatic") { // or "manual" or "denied"
const authorization = likeInteraction.createAuthorization({
id: new URL("https://example.com/authorizations/1"),
attributedTo: owner,
interactingObject: like,
interactionTarget: note,
});
// ...
}
}
The package does not install inbox listeners or store anything; the application decides where requests and authorizations live and when a human needs to approve. Verification fails closed. Without a policy, likes, replies, and announces are approved automatically, while quotes and features are denied. @fedify/vocab gains the FEP-7aa9 types this needs: FeaturedCollection, FeaturedItem, FeatureRequest, and FeatureAuthorization, along with actors' featuredCollections and InteractionPolicy.canFeature. Samuel Brinkmann asked for them in #810 once Mastodon 4.6 shipped the FEP. See the Interaction controls chapter; the work is in #914 and #929.
npm add @fedify/interaction-controls
pnpm add @fedify/interaction-controls
yarn add @fedify/interaction-controls
deno add jsr:@fedify/interaction-controls
bun add @fedify/interaction-controls
@fedify/netlify@fedify/netlify lets a Fedify application run on Netlify Functions without an external database or queue. NetlifyMessageQueue submits jobs as Netlify Async Workloads events, and createNetlifyQueueHandler() turns a Function into their consumer, with delayed delivery, native retries, and per-key FIFO ordering. NetlifyBlobsKvStore is a key–value store on Netlify Blobs with expiration, prefix listing, and compare-and-swap:
import {
createNetlifyQueueHandler,
NetlifyBlobsKvStore,
NetlifyMessageQueue,
} from "@fedify/netlify";
import { AsyncWorkloadsClient } from "@netlify/async-workloads";
import { getStore } from "@netlify/blobs";
export const kv = new NetlifyBlobsKvStore(
getStore({ name: "fedify", consistency: "strong" }),
);
export const queue = new NetlifyMessageQueue({
client: new AsyncWorkloadsClient(),
orderingKv: kv,
});
// netlify/functions/fedify-queue.ts
export default createNetlifyQueueHandler({
queue,
maxRetries: 4,
federation: () => builder.build({ kv, queue, manuallyStartQueue: true }),
});
The key–value store exists because of cost. #1010 describes a static blog whose ActivityPub traffic kept a Netlify Database from ever idling, which used most of the account's monthly credits. Jiwon Kwon contributed NetlifyBlobsKvStore in #1029, and the queue landed in #934. See Netlify Functions in the deployment guide.
npm add @fedify/netlify
pnpm add @fedify/netlify
yarn add @fedify/netlify
deno add jsr:@fedify/netlify
bun add @fedify/netlify
@fedify/pglite@fedify/pglite provides PgliteKvStore, a KvStore on an embedded PGlite database that you create and pass in. It uses the same schema as PostgresKvStore. It is meant for a single PGlite instance in a single runtime isolate, so it does not include a message queue: PGlite does not share data between processes.
import { PGlite } from "@electric-sql/pglite";
import { PgliteKvStore } from "@fedify/pglite";
const federation = createFederation({
kv: new PgliteKvStore(new PGlite("./data/fedify")),
});
ChanHaeng Lee contributed the package in #1020, together with testKvStore() in @fedify/testing. testKvStore() is a conformance suite for KvStore implementations, the counterpart to testMessageQueue(). See PgliteKvStore.
npm add @fedify/pglite
pnpm add @fedify/pglite
yarn add @fedify/pglite
deno add jsr:@fedify/pglite
bun add @fedify/pglite
Portable inbox forwarding, task deduplication, Netlify queue ordering, and the delivery circuit breaker all need an atomic compare-and-swap on the key–value store. PostgresKvStore now implements cas(), added in #934, and so does RedisKvStore, requested in #1163 and added in #1167. The Redis version uses a single-key Lua script, which works on standalone Redis and Redis Cluster. Before this, the circuit breaker fell back to racy updates on Redis.
Both come with conditions. RedisKvStore needs Redis to permit EVAL, and custom codecs must encode equal values identically. PostgresKvStore now creates logged tables by default, because losing sequence state in crash recovery could block queued events for good. It migrates existing unlogged tables when it initializes. That one-time migration rewrites the table and locks it exclusively, so schedule the upgrade accordingly if the table is large or busy; unlogged: true keeps the old behavior.
fedify init and the CLIfedify init can now scaffold a SvelteKit project, contributed by Jang Hanarae (@menele) in #971. Jang Hanarae also added a test task to generated projects in #990. It starts the app, waits for it to be ready, and checks that it resolves a local actor, which gives new projects a smoke test to run after scaffolding and after later changes. Before generating anything, fedify init now checks that the selected Deno, Bun, or Node.js meets Fedify's minimum version, or a framework's higher one, such as Astro's Node.js 22.12. It fails with a clear error in non-interactive mode, and in interactive mode it disables the affected package managers. Lee Jeongmin (@userjmmm) contributed the check in #981. Astro projects now use Astro 7, and since #936 @fedify/astro is tested against Astro 5, 6, and 7 with real builds on Node.js, Deno, and Bun, as #931 asked.
fedify tunnel gains fedify.com.es, a tunneling service run by the Fedify project that needs no account; the CLI pins its SSH host key and refuses a mismatched server before exposing a local port. localhost.run is removed in the same change, #940, because the service no longer exists. fedify lookup now times out each request after ten seconds when -T/--timeout is not given, and the option now also bounds the wait for a DNS lookup. The CLI moves to Optique 1.3.2 in #1197, which handles attached values like --timeout=30 consistently, gives better suggestions for mistyped options, and no longer hides errors for known options behind positional arguments before --.
@fedify/lint gains several rules beyond the media upload ones mentioned above. actor-preferred-username-required, requested in #895 and added in #1022, warns when an actor dispatcher returns an actor without preferredUsername. outbox-listener-delivery-not-awaited, from #1057 and #1067, reports an outbox listener that calls ctx.sendActivity() or ctx.forwardActivity() and drops the promise. On Cloudflare Workers, such an activity may never leave, because pending work is discarded once the response returns. await, return, Promise.all(), Promise.allSettled(), and waitUntil() count as handling the promise, while void, Promise.race(), and Promise.any() are accepted as a deliberate choice not to wait. The rule is a warning in the ESLint recommended configuration and an error in strict. Oxlint users enable it by name, and it is not available in Deno Lint.
In #1050, which resolves #900, the existing outbox-listener-delivery-required rule began following control flow instead of scanning the listener's source as text. It reports a listener whose only delivery calls sit in a dead branch, after an unconditional return or throw, or in a function that is never used, and it stays quiet when it cannot tell. Jae-Hyuk-Jang (@jhyuk) contributed this change and both new rules. archie (@ArchieTansaria) then taught both delivery rules about statically false loops and loop binding patterns in #1088. See the linting guide.
@fedify/vocab supports the FEP-22cd draft for translations. A Translation records a translated version's language, translators, source object, and source review time, and the new Object.translations property attaches them to a post without creating separate posts. See Translation metadata. (#1037, #1038)@fedify/vocab-tools gains an extraContext property option, which adds a JSON-LD context only when its terms are used, and a trustEmbeddedObjects type option. (#1037, #1038)suppressError: true handles are now logged as warnings rather than errors, so that they no longer show up as application errors. Contributed by Jae Hui Hong (@axz0612). (#933, #1035)enqueueMany() makes separate sends can set MessageQueue.atomicEnqueueMany to false. Fedify then rejects a multi-message batch governed by one deduplicationKey up front, since a partial send followed by a retry could enqueue duplicates. (#930, #934)Temporal bundle temporal-polyfill instead of requiring @js-temporal/polyfill at runtime, and type declarations rely on the standard esnext.temporal lib. (#823, #925)Accept header now includes the ActivityStreams profile on the JSON-LD media type, as ActivityPub and FEP-ef61 gateways require. (#834, #1077)fedify lookup --recurse reporting a timeout or other network failure as a possibly private object; it now reports the actual cause. (#1156, #1199)-p/--allow-private-address option of fedify webfinger being ignored. (#1156, #1199)outbox-listener-delivery-required and outbox-listener-delivery-not-awaited losing track of an object's functions when a nested helper assigns another property of that object. (#1125, #1140)npm add @fedify/fedify@2.4.0
pnpm add @fedify/fedify@2.4.0
yarn add @fedify/fedify@2.4.0
deno add jsr:@fedify/fedify@2.4.0
bun add @fedify/fedify@2.4.0
Update the other @fedify/* packages you use to 2.4.0 at the same time. To update the CLI, run npm install -g @fedify/cli@2.4.0, or see the installation instructions for other package managers.
Most applications need no code changes, but these changes in behavior may affect you:
401 Unauthorized unless they carry a valid Object Integrity Proof by the actor's DID. Keys at compatible identifiers, and keys at ordinary URLs that claim a portable owner, are no longer trusted by origin.Context as their options, such as await create.getObject(ctx), now verify the portable objects they dereference, including references to compatible identifiers from objects with ordinary HTTP(S) IDs./.well-known/apgateway/, and Context.sendActivity() may reject an activity that embeds others' portable objects. Next.js applications need the matcher shown above to serve hashlink media.maxHttpSignatures if you need more.fedify lookup does too.Tombstone or Object that return tombstones now respond with 410 Gone instead of 200 OK.PostgresKvStore migrates existing unlogged tables to logged tables on initialization, which locks the table.RedisKvStore needs EVAL permission for cas().Context and RequestContext interfaces need the new getPortable*Uri() methods and isSignedByAudience().fedify tunnel no longer supports localhost.run.Thanks to this release's contributors:
@fedify/adonisjs, and the request for FEP-7aa9 vocabularytest task in fedify initfedify initactor-preferred-username-required and outbox-listener-delivery-not-awaited lint rules, and control-flow analysis for outbox-listener-delivery-requiredChanHaeng Lee (@2chanhaeng) and Jiwon Kwon (@z9mb1) joined as maintainers after 2.3.0. ChanHaeng built the background task API and @fedify/pglite, Jiwon built NetlifyBlobsKvStore and rewrote the relay documentation, and both reviewed much of the rest of this release.
Many people added tests, documentation, and infrastructure during this cycle, often starting from a good first issue: Seoa Yoon (@bananamilk452), Lim Kyoujin (@dktsudgg), @m2nsp, Cho Seongmin (@seongmin36), Safal Shrestha (@Safal-Shrestha-SS), Jo Youngjae (@jaonz6057), @kecan0406, Loi Nguyen (@lntutor), Oh Taejun (@0w0), Yerin Park (@dstforye), Lee Dogeon (@moreal), and Sae Jin Kim (@siliconsjang).
The FEP-ef61 work owes a lot to people outside the project. @anotherdoesnm opened the request in #288. @silverpill, the author of FEP-ef61 and FEP-ae97, clarified the gateway trust model, reviewed the compound proof profile, and checked its test vectors. @dmitrizagidulin and @by_caballero weighed in on the profile's JSON-LD questions in #938. Testing against tootik and Mitra led to two of the interoperability fixes in this release.
See CHANGES.md for the complete changelog.
github.com
ActivityPub server framework in TypeScript. Contribute to fedify-dev/fedify development by creating an account on GitHub.
#ActivityPub #WebVTT #a11y #meta
#popcorn
Do you know any existing software using WebVTT for "time based content"?
WebVTT is used for subtitles by e.g. peertube but we can use it to show ActivityPub Content (e.g. a "Card") at any position to add context and background to an audio (in a <video>) or a video …
We intend to ship this in menschys @clemensg as well …
Fedify 2.4.0 is out.
The main addition is support for FEP-ef61 portable objects. A Fedify server can now host actors whose identity is a DID instead of a domain, serve their signed objects through /.well-known/apgateway/, and verify the portable objects of other servers. I tested it against tootik and Mitra, and wrote up where Fedify's profile differs from the FEP and what it leaves out.
The release also adds application-defined background tasks, the ActivityPub Media Upload extension, per-request inbox reports, 410 Gone for deleted objects, and four new packages (AdonisJS, Netlify, PGlite, and interaction controls).
Some defaults changed, so please read the upgrading section before you update, especially if you federate with software that uses FEP-ef61 compatible identifiers.
Release notes: https://hackers.pub/@fedify/2026/fedify-2-4.
Thanks to everyone who contributed to this cycle, and to @silverpill for patient answers about the FEP.
hackers.pub
Fedify is a TypeScript framework for building ActivityPub servers. It implements federation details such as HTTP Signatures, JSON-LD processing, WebFinger, inbox and outbox routing, and activity delivery.
Fedify 2.4.0 is a minor release with an unusually long changelog. The main addition is support for FEP-ef61 portable objects: a Fedify server can now host actors whose identity is a DID rather than a domain, serve and verify their signed objects, and consume the portable objects of other servers. The release also adds an API for application-defined background tasks, the ActivityPub Media Upload extension, a hook that reports how every inbox request was authenticated and handled, and 410 Gone tombstones for deleted objects. Several defaults change to limit what a hostile peer can make Fedify do. Four new packages ship with this version: @fedify/adonisjs, @fedify/interaction-controls, @fedify/netlify, and @fedify/pglite.
Most applications can upgrade without code changes, but a few of the new defaults are behavior changes. They are listed in the Upgrading section at the end.
An ActivityPub object's ID is an HTTP(S) URL on its server's domain. If that server shuts down, its IDs stop resolving, and an actor that moves elsewhere gets a new ID that everyone has to follow again. FEP-ef61, written by @silverpill of Mitra, proposes IDs that do not name a server at all. A portable ID is an ap: URI whose authority is a DID:
ap://did:key:z6MkrJVnaZkeFzdQyMZu1cgjg7k1pZZ6pvBQ7XJPt4swbTQ2/actor
The same object can be stored on several servers, called gateways, each of which serves it under /.well-known/apgateway/. Because no server owns the ID, a portable actor, activity, or object is authenticated only by its FEP-8b32 Object Integrity Proof, made with a key of the DID in its ID. A portable actor lists its gateways in a gateways property, and other servers deliver to its inbox through them.
Fedify 2.4.0 implements a profile of FEP-ef61 for gateways and consumers of portable objects. It makes two deliberate choices where the FEP or the specifications it rests on leave questions open, and it leaves out a few parts of the proposal; both are described below. The new Portable objects chapter of the manual walks through running portable actors from start to finish and then describes the profile as a whole, so that other implementers can tell what to expect from a Fedify server. The request came from @anotherdoesnm in #288, which tracked the work from a roadmap to the final pieces.
Portable objects are a foundation for nomadic identity, not nomadic identity itself. Fedify serves and verifies portable objects and forwards activities between gateways, but it does not copy actors or posts between servers, rotate keys, or move an actor to another DID. Full nomadic identity is still open in #413.
A portable actor starts with an Ed25519 key pair. Its DID is made from the public key with exportDidKey(), and whoever holds the private key can act as the actor, so store it with the actor and keep it safe:
import { generateCryptoKeyPair } from "@fedify/fedify";
import { exportDidKey } from "@fedify/vocab-runtime";
const { publicKey, privateKey } = await generateCryptoKeyPair("Ed25519");
const did = await exportDidKey(publicKey); // did:key:z6Mk...
// The verification method that the actor's proofs refer to:
const didKeyId = new URL(`${did}#${did.slice("did:key:".length)}`);
The actor dispatcher returns a portable actor as it would an ordinary one. Context.getPortableActorUri() and its siblings for inboxes, outboxes, collections, and objects build portable IDs from the dispatcher's paths and the DID, and signObject() signs the actor with the DID's key:
federation
.setActorDispatcher("/users/{identifier}", async (ctx, identifier) => {
const user = await findUser(identifier);
if (user == null) return null;
// A request through the gateway endpoint names the DID in its path,
// e.g., GET /.well-known/apgateway/did:key:z6Mk.../users/alice:
if (
ctx.portableRequest != null &&
ctx.portableRequest.authority !== user.did
) {
return null;
}
// This server's gateway keys for the actor (see below):
const keys = await ctx.getActorKeyPairs(identifier);
return await signObject(
new Person({
id: ctx.getPortableActorUri(identifier, user.did), // ap+ef61://did:key:z6Mk.../users/alice
preferredUsername: user.username,
inbox: ctx.getPortableInboxUri(identifier, user.did),
outbox: ctx.getPortableOutboxUri(identifier, user.did),
followers: ctx.getPortableFollowersUri(identifier, user.did),
gateways: [new URL("https://example.com")],
publicKeys: keys.map((key) => key.cryptographicKey),
assertionMethods: keys.map((key) => key.multikey),
}),
user.didKeyPair.privateKey,
new URL(`${user.did}#${user.did.slice("did:key:".length)}`),
);
})
.setKeyPairsDispatcher(async (ctx, identifier) => {
const user = await findUser(identifier);
// This server's own key pair for the actor, not the DID's:
return user == null ? [] : [user.gatewayKeyPair];
})
.mapPortableActorId(async (ctx, identifier) => {
const user = await findUser(identifier);
return user == null ? null : ctx.getPortableActorUri(identifier, user.did);
});
With this dispatcher, Fedify serves the actor at its compatible identifier, https://example.com/.well-known/apgateway/did:key:z6Mk.../users/alice, which is how other servers retrieve it. It serves it through WebFinger as @alice@example.com, since the handle's domain comes from the first gateway in gateways, and at its ordinary URL as before. On the gateway endpoint, Fedify serves a document only if its ID is the requested portable ID and it carries a proof made with a key of that DID.
The key pairs dispatcher and the new ActorCallbackSetters.mapPortableActorId() give the actor gateway keys. These are the server's own keys for the actor, and they sign only the HTTP requests the server makes on the actor's behalf, such as deliveries. They never sign the actor's activities or objects; only the DID's key does that. The actor document lists them under assertionMethods, as FEP-521a describes, so that other servers can verify the HTTP Signatures. See Dispatching the actor and Gateway keys of portable actors for the details.
Object and collection dispatchers serve portable objects and collections through the gateway endpoint in the same way, routed by the path after the DID. Two rules protect the actor's data. A non-public portable object is served only if the dispatcher has an authorization predicate, because FEP-ef61 forbids a gateway to show it to anyone outside its audience; the new RequestContext.isSignedByAudience() helps to write one. Without a predicate, a request for such an object gets 404 Not Found, as though the gateway did not store it. A deleted object is served as a signed Tombstone with 410 Gone. Resources that portable objects refer to by SHA-256 hashlinks, such as GET /.well-known/apgateway/hl:zQm..., are served by the new Federatable.setHashlinkMediaDispatcher().
A portable actor's activities have to be signed by its DID, which Fedify's key pairs dispatcher does not know about. Sign them with signObject() before sending, and pass normalizeExistingProofs: true so that the activity goes out in the form its proof covers. An activity that embeds a portable object, such as the Note of a Create, is signed inside out:
const actor = withGatewayHints(
ctx.getPortableActorUri(user.identifier, user.did),
[new URL("https://example.com")],
); // ap+ef61://did:key:z6Mk.../users/alice?@gateway=https%3A%2F%2Fexample.com
const note = await signObject(
new Note({
id: ctx.getPortableObjectUri(
Note,
{ identifier: user.identifier, id: crypto.randomUUID() },
user.did,
),
attribution: actor,
to: PUBLIC_COLLECTION,
content: "Hello, world!",
}),
user.didKeyPair.privateKey,
didKeyId,
);
const create = await signObject(
new Create({
id: ctx.getPortableObjectUri(
Note,
{ identifier: user.identifier, id: crypto.randomUUID() },
user.did,
),
actor,
to: PUBLIC_COLLECTION,
object: note,
}),
user.didKeyPair.privateKey,
didKeyId,
);
await ctx.sendActivity(
{ identifier: user.identifier },
"followers",
create,
{ normalizeExistingProofs: true },
);
This example depends on a fix to signObject() that is useful outside FEP-ef61 as well. Previously, a signed object assigned to a typed parent was rebuilt under the parent's JSON-LD context when the parent was serialized. The child kept its proofValue but lost the document its proof covered, so it no longer verified on its own. signObject() now captures the exact document it signed, and nested serialization embeds that document verbatim. The captured document is a snapshot: clone() does not carry it, so sign a clone again if it has to be embedded. The design is in #1044 and the fix in #1051.
Context.sendActivity() refuses a portable activity that is unsigned, signed by the wrong key, or carries a Linked Data Signature, and throws a TypeError before anything is queued. It also refuses a few shapes of compound document that Fedify's own inboxes would reject, such as a received signed Follow embedded in an Accept; refer to such an object by its ID instead. Deliveries to portable inboxes go through the recipient's gateways, or else the @gateway hints on the inbox, trying at most five of them until one accepts. Before this release, they failed with UrlError: Unsupported protocol: ap+ef61:. The rules are in Delivering to portable actors and Producing a compound document.
On the receiving side, a portable inbox is reached as POST /.well-known/apgateway/did:key:z6Mk.../users/alice/inbox and dispatched to the same inbox listeners as the ordinary inbox. Context.parseUri() gains a portable option, and the parsed result has the DID in a new authority property to check against the one you store. Fedify accepts an activity of a portable actor, or one with a portable ID, only if it has a valid proof by that DID; neither an HTTP Signature nor a Linked Data Signature authenticates it. Each portable object embedded in the activity is verified against its own proof before dispatch. An accepted activity that carries its own proof is forwarded to the actor's other gateways at most once, as FEP-ef61 recommends. Forwarding needs a key–value store that supports cas(), and the FederationOptions.portableInboxForwarding option tunes it. See Portable inboxes and Forwarding to other gateways.
Applications that never host a portable actor still meet portable objects from other servers. Context.lookupObject() resolves portable IDs, compatible identifiers, and the fediverse handles of portable actors, and returns portable objects only if they verify:
const actor = await ctx.lookupObject("@alice@example.com");
// A portable ID without @gateway hints needs gateways to ask:
const note = await ctx.lookupObject("ap://did:key:z6Mk.../notes/1", {
gateways: ["https://example.com"],
});
Property accessors fetch portable references through gateways as well, and they verify what they fetch with the new verifyPortableObject option. Context has a matching verifyPortableObject property, so passing a context as the options, as in await create.getObject(ctx), is enough. Activities received in inboxes use this verifier by default, and objects fetched through their accessors inherit it, so a chained call such as (await create.getObject(ctx))?.getAttribution() verifies each portable object along the way. A lookup asks at most five gateways, falling back to the next when one fails or serves something that does not verify. Unsigned collections are accepted only from a gateway that the owner lists, and only for an actor's inbox, outbox, followers, following, and liked, as FEP-ef61 allows. fetchPortableMedia() retrieves the media of a portable object and returns it only if it matches the object's digestMultibase. See Dereferencing portable references and Default verifiers.
Software that does not understand ap: URIs can refer to a portable object by a compatible identifier, an HTTP(S) URL under a gateway's /.well-known/apgateway/ path such as https://gw.example/.well-known/apgateway/did:key:z6Mk.../actor. Some implementations, tootik among them, use compatible identifiers as the IDs of their portable actors.
Earlier versions of Fedify treated these URLs as ordinary web URLs and trusted them by the server that served them. Since anyone can serve a compatible identifier for any DID, https://evil.example/.well-known/apgateway/did:key:z6MkAlice/actor could act as Alice without a proof by Alice's DID. Fedify 2.4.0 treats a compatible identifier as the portable object it stands for, as FEP-ef61 requires. Inboxes reject an activity from an actor whose ID is a compatible identifier with 401 Unauthorized unless it has a valid Object Integrity Proof by the actor's DID, and there is no option to turn this off. Actors that publish compatible identifiers without signing them are no longer accepted, and neither are those whose DIDs use key types that Fedify cannot verify yet. The change was tracked in #1093 and made in #1105.
Your own portable actors may still use compatible identifiers as their IDs, for software that rejects ap: IDs. Build them with toCompatibleEf61Id() before signing, and Fedify will treat them as portable wherever it serves or sends them. It also warns if one is malformed or does not point at the actor's first gateway. See Compatible identifiers as actor IDs.
Two choices in Fedify's profile differ from the current FEP text or rest on semantics that are not settled yet. Fedify keeps both for the 2.x series, and either may change in Fedify 3.0.
The first is the scheme. FEP-ef61 recommends ap: and canonicalizes ap+ef61: to it, while also warning that the recommendation may change to ap+ef61:, since these IDs are meant only for portable objects. Fedify accepts both, compares them as equal, and serializes ap+ef61:. Parsing a document normalizes its portable IRIs to ap+ef61:, so re-serializing an object that another implementation signed with ap: IDs breaks its proof. Keep the received JSON of such documents, and compare portable IDs with arePortableUrisEqual() rather than as strings. The discussion is in #826 and #828, and the reasoning is in Canonical ap+ef61: scheme.
The second concerns compound documents. When a signed Note sits inside a signed Create, the verifier has to know which part of the JSON each proof covers, and neither FEP-8b32 nor Verifiable Credential Data Integrity defines that yet; the question is open in w3c/vc-data-integrity#350. Fedify uses an interim map-local profile, worked out in #938. Each JSON map with a proof is its own secured document, each embedded portable object needs its own proof and its own @context, and proof sets and chains are not supported. The profile authenticates the exact JSON values each proof covers; it does not promise that a child's JSON-LD expansion inside its parent matches its expansion on its own. @silverpill, @dmitrizagidulin, and @by_caballero reviewed the proposal in that thread, and @silverpill reproduced its test vectors independently. See Map-local compound proofs.
Fedify does not implement FEP-ae97, the companion proposal for client-side signing. There are no endpoints for registering actors, submitting client-signed activities to a portable outbox, or gateway discovery, and no media upload and deletion endpoints under /.well-known/apgateway-media. Your own outbox listeners can still relay activities that a client signed, unchanged. Fedify also does not copy or reconcile actors, objects, or collections between gateways, so running an actor on several gateways means keeping their data in sync yourself. There is no workflow for rotating a DID's key or migrating an actor to another DID. Fedify resolves verification methods only for did:key DIDs with Ed25519 keys; other DID methods verify only if your document loader resolves them. Gateways must be plain origins, since the arbitrary gateway paths that FEP-ef61 discusses are not supported. The full list is in Not supported.
The profile was tested against tootik v0.25.4 and Mitra v5.10.0 in #1202. Fedify verifies tootik's portable actors, activities, and HTTP Signatures, and Mitra's gateway HTTP Signatures on activities that a client signed. Both accept the portable activities that Fedify sends. Two fixes came out of this testing. Fedify now reads a gateways list that tootik leaves unmapped in its JSON-LD context. It also serializes gateways as bare origins, because Mitra rejects a trailing slash. Fedify has not yet verified an actor or activity that Mitra signed itself, and Mastodon's handling of portable actors has not been tested. The FEP-ef61 interoperability section of FEDERATION.md records what was tested and how.
fedify lookup looks up portable objects by their ap: and ap+ef61: IDs, compatible identifiers, and the handles of portable actors, and it verifies their proofs. When no gateway returns an acceptable object, it says why. A new --gateway option on fedify lookup, fedify inbox, and fedify webfinger names the gateways to ask. fedify inbox --follow follows portable actors, and fedify webfinger accepts portable actor IDs. See Looking up portable objects in the CLI manual.
The mock federation and contexts in @fedify/testing support portable IDs, verification against fixture document loaders, gateway requests, and hashlink media, so tests can exercise portable objects without a live gateway. @fedify/lint's actor URI rules accept the portable IDs that the getPortable*Uri() methods build. @fedify/next now passes hashlink media requests to Fedify, a problem reported in #1149 and fixed in #1170; to have Next.js run the middleware for them, add { source: "/.well-known/apgateway/:path*" } to your matcher.
Fedify already runs activity delivery and inbox processing on queue workers, but applications usually have jobs of their own: digest emails, timeline rebuilds, link previews. Until now, those jobs could not use Fedify's queues, serialization, or retries. Fedify 2.4.0 opens the same machinery to application-defined tasks.
A task is defined once with defineTask() and enqueued from any Context with enqueueTask() or enqueueTaskMany(). Every task takes a Standard Schema, such as a Zod or Valibot schema, which types its payload. Fedify validates the payload when it is enqueued and again when a worker picks it up; the second check catches payloads from an earlier deployment that no longer match the schema:
import { createExponentialBackoffPolicy } from "@fedify/fedify";
import { z } from "zod";
const sendDigest = federation.defineTask("sendDigest", {
schema: z.object({ userId: z.string(), since: z.date() }),
handler: async (ctx, data) => {
// data is typed as { userId: string; since: Date }
await sendEmail(data.userId, await buildDigest(data.userId, data.since));
},
retryPolicy: createExponentialBackoffPolicy({ maxAttempts: 3 }),
});
// Anywhere with a Context:
await ctx.enqueueTask(sendDigest, { userId: "alice", since: new Date() }, {
delay: { minutes: 30 },
deduplicationKey: "digest:alice",
});
Fedify serializes payloads with devalue, so Date, Map, Set, URL, bigint, Temporal values, circular references, and Activity Vocabulary objects all survive the trip through any message queue backend. A Note in a payload comes back as a Note. Failed handlers are retried with exponential backoff by default, and queues with native retries handle retries themselves. Tasks fall back to the outbox queue unless you give them their own. Use the new task slot of the queue option or a per-task queue for that, and startQueue(ctxData, { queue: "task" }) runs a task-only worker. Setting taskQueueResolution: "strict" turns the fallback into an error.
A deduplicationKey deduplicates enqueues. The feature came from @julian's point in #206 that recurring tasks are hard to manage when you cannot tell whether one is already queued. A queue that declares nativeDeduplication enforces the key itself; otherwise, Fedify keeps a best-effort marker in the key–value store through cas(). Each task runs in a fedify.task span, and the queue metrics from 2.3.0 now include task queues. The Background tasks chapter covers routing, retries, deduplication, and the limitations; cron-style scheduling, result backends, and priorities are out of scope for now.
ChanHaeng Lee (@2chanhaeng) implemented the API: the core in #803, deduplication in #806, and observability in #812, following the plans in #797, #798, and #799. The work was merged in #923, closing #206, which had been open since February 2025.
Fedify 2.2.0 added client-to-server outbox listeners, but a C2S client also has to upload images and video before it can post them. That traffic does not go through POST /outbox. Fedify 2.4.0 supports the ActivityPub Media Upload extension, a SocialCG proposal last updated in 2017 that never reached W3C Recommendation status.
setMediaUploader() registers a multipart/form-data endpoint. Its callback receives the uploaded file and the object shell the client posted along with it. If the callback returns the created object, Fedify responds with 201 Created; if it returns a URL where the object will appear once processing finishes, Fedify responds with 202 Accepted:
federation
.setMediaUploader(
"/users/{identifier}/media",
async (ctx, identifier, file, object) => {
const stored = await uploadToStorage(file);
return new Image({
id: ctx.getObjectUri(Image, { uuid: stored.uuid }),
url: new URL(stored.publicUrl),
mediaType: file.type,
name: object.name,
});
},
)
.authorize(async (ctx, identifier) => {
const session = await verifyAccessToken(
ctx.request.headers.get("authorization"),
);
return session?.identifier === identifier;
});
The actor dispatcher advertises the endpoint itself by setting endpoints: new Endpoints({ uploadMedia: ctx.getMediaUploaderUri(identifier) }), the same way it places its inbox and outbox URIs. Without an authorize() hook, anyone who finds the endpoint can upload to it. For that reason, Fedify logs a warning when an uploader is registered without one, and when the returned object's URI does not match an object dispatcher or the actor does not advertise the endpoint. Four new @fedify/lint rules catch the same mistakes before they run. Fedify reads the upload into memory and sets no size limit of its own, so cap the request size at your reverse proxy. Storing the file and serving the object back are up to the application. See the Media upload chapter; the work is in #754 and #927.
Fedify decides several things about every inbox delivery: which signature mechanisms applied, which keys they tried, whether the actor owned the key, and what happened to the activity. Before this release, applications saw only part of that. A Linked Data Signature by a key the actor did not own was counted the same as a bad signature, and the key that verified a request was never handed to the application. The request in #1191 came from an effort to log these decisions for a debugging tool.
The new onRequestFinished() callback on inbox listeners receives an InboxRequestReport for every delivery to an inbox route, including rejected requests and requests that fail before verification:
federation
.setInboxListeners("/users/{identifier}/inbox", "/inbox")
.on(Follow, async (ctx, follow) => { /* ... */ })
.onRequestFinished(async (ctx, report) => {
await db.insertInboxLog({
inbox: report.inbox.kind, // "personal" | "shared" | "portable"
authentication: report.authentication.status,
outcome: report.outcome.type === "response"
? report.outcome.disposition // "processed" | "enqueued" | "duplicate" | ...
: `exception:${report.outcome.stage}`,
keyIds: report.attempts
.flatMap((attempt) => attempt.checks)
.flatMap((check) => check.triedKeys)
.map((key) => key.id?.href),
});
});
Each HTTP Signature, Linked Data Signature, or Object Integrity Proof check appears as an attempt with the actual keys it tried, including a stale cached key and its refreshed replacement. The report also carries the final authentication decision, such as actorKeyMismatch or proofPolicy, and the response or exception that ended the request. Fedify awaits the callback whether or not the trace is sampled. Errors it throws are logged without changing the response, and enqueued means the queue accepted the activity, not that a worker later processed it. Mock federations in @fedify/testing accept the same callback. See Observing inbox requests; the implementation is in #1201.
Actor dispatchers have been able to return a Tombstone since 2.2.0, but object dispatchers could only return null. A deleted post was answered with 404 Not Found, as if it had never existed, so applications that wanted 410 Gone, as ActivityPub recommends, needed their own route in front of Fedify.
Object dispatchers may now return a Tombstone, which Fedify serves with 410 Gone and the serialized tombstone after applying the authorization predicate:
federation.setObjectDispatcher(
Note,
"/users/{identifier}/notes/{id}",
async (ctx, values) => {
const post = await getPost(values.identifier, values.id);
if (post == null) return null;
if (post.deletedAt != null) {
return new Tombstone({
id: ctx.getObjectUri(Note, values),
formerType: Note,
deleted: post.deletedAt,
});
}
return new Note({ id: ctx.getObjectUri(Note, values), content: post.content });
},
);
The return type of ObjectDispatcher widens to TObject | Tombstone | null. RequestContext.getObject() still returns null for a tombstone unless you pass { tombstone: "passthrough" } or ask for the Tombstone or Object class itself. Dispatchers registered for Tombstone or Object that already returned tombstones now respond with 410 rather than 200. See Deleted objects; the change was proposed in #1112 and made in #1117.
Much of what Fedify fetches is chosen by the sender of an incoming request. Key IDs in signatures are one example, and the servers behind them can be slow on purpose. Several changes in 2.4.0 bound that work.
As #1130 points out, a request may carry several RFC 9421 signatures, and each one can make Fedify fetch a key from a URL of the sender's choosing. Fedify now verifies at most the first three, in Signature-Input order, and ignores the rest. A signature counts toward the limit even if it fails before its key is fetched, and a key named by several signatures is looked up once. The new FederationOptions.maxHttpSignatures option, added with the limit in #1166, changes it.
The built-in document loaders now give each call ten seconds by default, a limit proposed in #1131 and added in #1169. The time limit covers every redirect, alternate document, double-knocking retry, and the response body. A timed-out call throws a FetchError whose cause is a DOMException named "TimeoutError". Error responses are read in full before the loader throws, up to 1 MiB, so that a server cannot send a 404 header and then stall the body. FederationOptions.documentLoaderTimeout changes the limit or turns it off with null, and custom document loaders are not affected. Since #913, getDocumentLoader() also rejects HTML responses that do not advertise an ActivityPub alternate with a FetchError, rather than failing inside JSON.parse(), which #912 found cluttering error trackers.
Cached public keys and remembered HTTP Message Signatures specs used to live forever in the key–value store. #1017 reports a production Redis instance holding 194,424 public-key entries, about 165 MiB, for actors that had mostly stopped federating. Keys now expire after 30 days and specs after 90, configurable through FederationOptions.publicKeyTtl and FederationOptions.httpMessageSignaturesSpecTtl. Entries written by earlier versions have no expiry until they are next written. The Clearing legacy cache entries section shows how to remove them for Redis, PostgreSQL, and other stores. Heewon Chae (@heeoneie) implemented this in #1027, their first contribution to Fedify.
const federation = createFederation<void>({
kv,
maxHttpSignatures: 1, // default: 3
documentLoaderTimeout: { seconds: 5 }, // default: 10 seconds
publicKeyTtl: { days: 7 }, // default: 30 days
httpMessageSignaturesSpecTtl: { days: 30 }, // default: 90 days
});
Finally, verifyProof() now authenticates every option of an Ed25519 JCS proof except proofValue, including expires, domain, challenge, and nonce. It also rejects expired or malformed ones. Previously, a field such as expires could be added to an already signed proof without invalidating it. Jiwon Kwon (@z9mb1) spotted this in review of #968. Callers can now require a domain and challenge through VerifyProofOptions to prevent cross-domain or replayed use.
@fedify/adonisjs@fedify/adonisjs integrates Fedify with AdonisJS. Running node ace add @fedify/adonisjs registers a service provider that owns the federation's lifecycle and a server middleware that mounts it. It also scaffolds config/fedify.ts, start/federation.ts, and app/federation/main.ts. Controllers get a ctx.federation request context:
// app/federation/main.ts
import federation from "@fedify/adonisjs/services/builder";
import { Person } from "@fedify/vocab";
federation.setActorDispatcher("/actors/{identifier}", (ctx, identifier) =>
new Person({ id: ctx.getActorUri(identifier), preferredUsername: identifier }));
The middleware sits first in AdonisJS's server stack rather than its router stack. That way Fedify sees paths the router does not know, such as /.well-known/webfinger, and reads request bodies before the body parser consumes them. The package targets Node.js and is published to npm only. See the AdonisJS section of the integration guide.
Samuel Brinkmann (@sabrinkmann) contributed the package in #1006, closing #139, and built it on a prototype that Emelia Smith (@thisismissem) attached to that issue.
npm add @fedify/adonisjs
pnpm add @fedify/adonisjs
yarn add @fedify/adonisjs
@fedify/interaction-controlsGoToSocial interaction controls, FEP-044f quote authorization, and FEP-7aa9 featured collections share a request-and-authorization flow. A server asks to reply to, quote, or feature a post, and the author's server approves according to the post's interactionPolicy. @fedify/interaction-controls implements that flow for like, reply, announce, quote, and feature interactions. Each has a helper, such as likeInteraction or quoteInteraction, that creates and verifies requests and authorizations, evaluates policies, and recognizes interactions that arrived without a request:
import { likeInteraction } from "@fedify/interaction-controls";
// When a LikeRequest arrives:
const verified = await likeInteraction.verifyRequest(ctx, { request });
if (verified.verified) {
const decision = await likeInteraction.evaluatePolicy(ctx, {
subject: note,
requester: request.actorId!,
});
if (decision.result === "automatic") { // or "manual" or "denied"
const authorization = likeInteraction.createAuthorization({
id: new URL("https://example.com/authorizations/1"),
attributedTo: owner,
interactingObject: like,
interactionTarget: note,
});
// ...
}
}
The package does not install inbox listeners or store anything; the application decides where requests and authorizations live and when a human needs to approve. Verification fails closed. Without a policy, likes, replies, and announces are approved automatically, while quotes and features are denied. @fedify/vocab gains the FEP-7aa9 types this needs: FeaturedCollection, FeaturedItem, FeatureRequest, and FeatureAuthorization, along with actors' featuredCollections and InteractionPolicy.canFeature. Samuel Brinkmann asked for them in #810 once Mastodon 4.6 shipped the FEP. See the Interaction controls chapter; the work is in #914 and #929.
npm add @fedify/interaction-controls
pnpm add @fedify/interaction-controls
yarn add @fedify/interaction-controls
deno add jsr:@fedify/interaction-controls
bun add @fedify/interaction-controls
@fedify/netlify@fedify/netlify lets a Fedify application run on Netlify Functions without an external database or queue. NetlifyMessageQueue submits jobs as Netlify Async Workloads events, and createNetlifyQueueHandler() turns a Function into their consumer, with delayed delivery, native retries, and per-key FIFO ordering. NetlifyBlobsKvStore is a key–value store on Netlify Blobs with expiration, prefix listing, and compare-and-swap:
import {
createNetlifyQueueHandler,
NetlifyBlobsKvStore,
NetlifyMessageQueue,
} from "@fedify/netlify";
import { AsyncWorkloadsClient } from "@netlify/async-workloads";
import { getStore } from "@netlify/blobs";
export const kv = new NetlifyBlobsKvStore(
getStore({ name: "fedify", consistency: "strong" }),
);
export const queue = new NetlifyMessageQueue({
client: new AsyncWorkloadsClient(),
orderingKv: kv,
});
// netlify/functions/fedify-queue.ts
export default createNetlifyQueueHandler({
queue,
maxRetries: 4,
federation: () => builder.build({ kv, queue, manuallyStartQueue: true }),
});
The key–value store exists because of cost. #1010 describes a static blog whose ActivityPub traffic kept a Netlify Database from ever idling, which used most of the account's monthly credits. Jiwon Kwon contributed NetlifyBlobsKvStore in #1029, and the queue landed in #934. See Netlify Functions in the deployment guide.
npm add @fedify/netlify
pnpm add @fedify/netlify
yarn add @fedify/netlify
deno add jsr:@fedify/netlify
bun add @fedify/netlify
@fedify/pglite@fedify/pglite provides PgliteKvStore, a KvStore on an embedded PGlite database that you create and pass in. It uses the same schema as PostgresKvStore. It is meant for a single PGlite instance in a single runtime isolate, so it does not include a message queue: PGlite does not share data between processes.
import { PGlite } from "@electric-sql/pglite";
import { PgliteKvStore } from "@fedify/pglite";
const federation = createFederation({
kv: new PgliteKvStore(new PGlite("./data/fedify")),
});
ChanHaeng Lee contributed the package in #1020, together with testKvStore() in @fedify/testing. testKvStore() is a conformance suite for KvStore implementations, the counterpart to testMessageQueue(). See PgliteKvStore.
npm add @fedify/pglite
pnpm add @fedify/pglite
yarn add @fedify/pglite
deno add jsr:@fedify/pglite
bun add @fedify/pglite
Portable inbox forwarding, task deduplication, Netlify queue ordering, and the delivery circuit breaker all need an atomic compare-and-swap on the key–value store. PostgresKvStore now implements cas(), added in #934, and so does RedisKvStore, requested in #1163 and added in #1167. The Redis version uses a single-key Lua script, which works on standalone Redis and Redis Cluster. Before this, the circuit breaker fell back to racy updates on Redis.
Both come with conditions. RedisKvStore needs Redis to permit EVAL, and custom codecs must encode equal values identically. PostgresKvStore now creates logged tables by default, because losing sequence state in crash recovery could block queued events for good. It migrates existing unlogged tables when it initializes. That one-time migration rewrites the table and locks it exclusively, so schedule the upgrade accordingly if the table is large or busy; unlogged: true keeps the old behavior.
fedify init and the CLIfedify init can now scaffold a SvelteKit project, contributed by Jang Hanarae (@menele) in #971. Jang Hanarae also added a test task to generated projects in #990. It starts the app, waits for it to be ready, and checks that it resolves a local actor, which gives new projects a smoke test to run after scaffolding and after later changes. Before generating anything, fedify init now checks that the selected Deno, Bun, or Node.js meets Fedify's minimum version, or a framework's higher one, such as Astro's Node.js 22.12. It fails with a clear error in non-interactive mode, and in interactive mode it disables the affected package managers. Lee Jeongmin (@userjmmm) contributed the check in #981. Astro projects now use Astro 7, and since #936 @fedify/astro is tested against Astro 5, 6, and 7 with real builds on Node.js, Deno, and Bun, as #931 asked.
fedify tunnel gains fedify.com.es, a tunneling service run by the Fedify project that needs no account; the CLI pins its SSH host key and refuses a mismatched server before exposing a local port. localhost.run is removed in the same change, #940, because the service no longer exists. fedify lookup now times out each request after ten seconds when -T/--timeout is not given, and the option now also bounds the wait for a DNS lookup. The CLI moves to Optique 1.3.2 in #1197, which handles attached values like --timeout=30 consistently, gives better suggestions for mistyped options, and no longer hides errors for known options behind positional arguments before --.
@fedify/lint gains several rules beyond the media upload ones mentioned above. actor-preferred-username-required, requested in #895 and added in #1022, warns when an actor dispatcher returns an actor without preferredUsername. outbox-listener-delivery-not-awaited, from #1057 and #1067, reports an outbox listener that calls ctx.sendActivity() or ctx.forwardActivity() and drops the promise. On Cloudflare Workers, such an activity may never leave, because pending work is discarded once the response returns. await, return, Promise.all(), Promise.allSettled(), and waitUntil() count as handling the promise, while void, Promise.race(), and Promise.any() are accepted as a deliberate choice not to wait. The rule is a warning in the ESLint recommended configuration and an error in strict. Oxlint users enable it by name, and it is not available in Deno Lint.
In #1050, which resolves #900, the existing outbox-listener-delivery-required rule began following control flow instead of scanning the listener's source as text. It reports a listener whose only delivery calls sit in a dead branch, after an unconditional return or throw, or in a function that is never used, and it stays quiet when it cannot tell. Jae-Hyuk-Jang (@jhyuk) contributed this change and both new rules. archie (@ArchieTansaria) then taught both delivery rules about statically false loops and loop binding patterns in #1088. See the linting guide.
@fedify/vocab supports the FEP-22cd draft for translations. A Translation records a translated version's language, translators, source object, and source review time, and the new Object.translations property attaches them to a post without creating separate posts. See Translation metadata. (#1037, #1038)@fedify/vocab-tools gains an extraContext property option, which adds a JSON-LD context only when its terms are used, and a trustEmbeddedObjects type option. (#1037, #1038)suppressError: true handles are now logged as warnings rather than errors, so that they no longer show up as application errors. Contributed by Jae Hui Hong (@axz0612). (#933, #1035)enqueueMany() makes separate sends can set MessageQueue.atomicEnqueueMany to false. Fedify then rejects a multi-message batch governed by one deduplicationKey up front, since a partial send followed by a retry could enqueue duplicates. (#930, #934)Temporal bundle temporal-polyfill instead of requiring @js-temporal/polyfill at runtime, and type declarations rely on the standard esnext.temporal lib. (#823, #925)Accept header now includes the ActivityStreams profile on the JSON-LD media type, as ActivityPub and FEP-ef61 gateways require. (#834, #1077)fedify lookup --recurse reporting a timeout or other network failure as a possibly private object; it now reports the actual cause. (#1156, #1199)-p/--allow-private-address option of fedify webfinger being ignored. (#1156, #1199)outbox-listener-delivery-required and outbox-listener-delivery-not-awaited losing track of an object's functions when a nested helper assigns another property of that object. (#1125, #1140)npm add @fedify/fedify@2.4.0
pnpm add @fedify/fedify@2.4.0
yarn add @fedify/fedify@2.4.0
deno add jsr:@fedify/fedify@2.4.0
bun add @fedify/fedify@2.4.0
Update the other @fedify/* packages you use to 2.4.0 at the same time. To update the CLI, run npm install -g @fedify/cli@2.4.0, or see the installation instructions for other package managers.
Most applications need no code changes, but these changes in behavior may affect you:
401 Unauthorized unless they carry a valid Object Integrity Proof by the actor's DID. Keys at compatible identifiers, and keys at ordinary URLs that claim a portable owner, are no longer trusted by origin.Context as their options, such as await create.getObject(ctx), now verify the portable objects they dereference, including references to compatible identifiers from objects with ordinary HTTP(S) IDs./.well-known/apgateway/, and Context.sendActivity() may reject an activity that embeds others' portable objects. Next.js applications need the matcher shown above to serve hashlink media.maxHttpSignatures if you need more.fedify lookup does too.Tombstone or Object that return tombstones now respond with 410 Gone instead of 200 OK.PostgresKvStore migrates existing unlogged tables to logged tables on initialization, which locks the table.RedisKvStore needs EVAL permission for cas().Context and RequestContext interfaces need the new getPortable*Uri() methods and isSignedByAudience().fedify tunnel no longer supports localhost.run.Thanks to this release's contributors:
@fedify/adonisjs, and the request for FEP-7aa9 vocabularytest task in fedify initfedify initactor-preferred-username-required and outbox-listener-delivery-not-awaited lint rules, and control-flow analysis for outbox-listener-delivery-requiredChanHaeng Lee (@2chanhaeng) and Jiwon Kwon (@z9mb1) joined as maintainers after 2.3.0. ChanHaeng built the background task API and @fedify/pglite, Jiwon built NetlifyBlobsKvStore and rewrote the relay documentation, and both reviewed much of the rest of this release.
Many people added tests, documentation, and infrastructure during this cycle, often starting from a good first issue: Seoa Yoon (@bananamilk452), Lim Kyoujin (@dktsudgg), @m2nsp, Cho Seongmin (@seongmin36), Safal Shrestha (@Safal-Shrestha-SS), Jo Youngjae (@jaonz6057), @kecan0406, Loi Nguyen (@lntutor), Oh Taejun (@0w0), Yerin Park (@dstforye), Lee Dogeon (@moreal), and Sae Jin Kim (@siliconsjang).
The FEP-ef61 work owes a lot to people outside the project. @anotherdoesnm opened the request in #288. @silverpill, the author of FEP-ef61 and FEP-ae97, clarified the gateway trust model, reviewed the compound proof profile, and checked its test vectors. @dmitrizagidulin and @by_caballero weighed in on the profile's JSON-LD questions in #938. Testing against tootik and Mitra led to two of the interoperability fixes in this release.
See CHANGES.md for the complete changelog.
github.com
ActivityPub server framework in TypeScript. Contribute to fedify-dev/fedify development by creating an account on GitHub.
is there an exhaustive list of client-2-server apis for #ActivityPub ?
30 Sep 2026
Activity Relay 3.0.1 has been released to fix a couple bugs caused by invalid follower states blocking relay operation.
https://github.com/thystra/Activity-Relay/releases/tag/v3.0.1
Activity-Relay 3.0.1 is a focused reliability release for follower-state handling and relay fan-out.
No configuration or data migration is required.
The 3.0.1 candidate was production-tested on relay.argentwolf.org, including real WordPress fan-out to healthy Friendica and Mastodon receivers while an independently failing receiver continued through its isolated retry path.
Container: ghcr.io/thystra/activity-relay:3.0.1
See docs/releases/v3.0.1.md and CHANGELOG.md for complete details.
The attached files are the exact canonical artifacts produced and accepted by the Forgejo release workflow.
If you find this useful, please consider leaving a tip on my Ko-Fi – https://ko-fi.com/thewolfandtheraven
#activitypub #fediverse #friendica #mastodon #nodebb #relay #relays #relayserver
Fedify 2.4.0 is out.
The main addition is support for FEP-ef61 portable objects. A Fedify server can now host actors whose identity is a DID instead of a domain, serve their signed objects through /.well-known/apgateway/, and verify the portable objects of other servers. I tested it against tootik and Mitra, and wrote up where Fedify's profile differs from the FEP and what it leaves out.
The release also adds application-defined background tasks, the ActivityPub Media Upload extension, per-request inbox reports, 410 Gone for deleted objects, and four new packages (AdonisJS, Netlify, PGlite, and interaction controls).
Some defaults changed, so please read the upgrading section before you update, especially if you federate with software that uses FEP-ef61 compatible identifiers.
Release notes: https://hackers.pub/@fedify/2026/fedify-2-4.
Thanks to everyone who contributed to this cycle, and to @silverpill for patient answers about the FEP.
hackers.pub
Fedify is a TypeScript framework for building ActivityPub servers. It implements federation details such as HTTP Signatures, JSON-LD processing, WebFinger, inbox and outbox routing, and activity delivery.
Fedify 2.4.0 is a minor release with an unusually long changelog. The main addition is support for FEP-ef61 portable objects: a Fedify server can now host actors whose identity is a DID rather than a domain, serve and verify their signed objects, and consume the portable objects of other servers. The release also adds an API for application-defined background tasks, the ActivityPub Media Upload extension, a hook that reports how every inbox request was authenticated and handled, and 410 Gone tombstones for deleted objects. Several defaults change to limit what a hostile peer can make Fedify do. Four new packages ship with this version: @fedify/adonisjs, @fedify/interaction-controls, @fedify/netlify, and @fedify/pglite.
Most applications can upgrade without code changes, but a few of the new defaults are behavior changes. They are listed in the Upgrading section at the end.
An ActivityPub object's ID is an HTTP(S) URL on its server's domain. If that server shuts down, its IDs stop resolving, and an actor that moves elsewhere gets a new ID that everyone has to follow again. FEP-ef61, written by @silverpill of Mitra, proposes IDs that do not name a server at all. A portable ID is an ap: URI whose authority is a DID:
ap://did:key:z6MkrJVnaZkeFzdQyMZu1cgjg7k1pZZ6pvBQ7XJPt4swbTQ2/actor
The same object can be stored on several servers, called gateways, each of which serves it under /.well-known/apgateway/. Because no server owns the ID, a portable actor, activity, or object is authenticated only by its FEP-8b32 Object Integrity Proof, made with a key of the DID in its ID. A portable actor lists its gateways in a gateways property, and other servers deliver to its inbox through them.
Fedify 2.4.0 implements a profile of FEP-ef61 for gateways and consumers of portable objects. It makes two deliberate choices where the FEP or the specifications it rests on leave questions open, and it leaves out a few parts of the proposal; both are described below. The new Portable objects chapter of the manual walks through running portable actors from start to finish and then describes the profile as a whole, so that other implementers can tell what to expect from a Fedify server. The request came from @anotherdoesnm in #288, which tracked the work from a roadmap to the final pieces.
Portable objects are a foundation for nomadic identity, not nomadic identity itself. Fedify serves and verifies portable objects and forwards activities between gateways, but it does not copy actors or posts between servers, rotate keys, or move an actor to another DID. Full nomadic identity is still open in #413.
A portable actor starts with an Ed25519 key pair. Its DID is made from the public key with exportDidKey(), and whoever holds the private key can act as the actor, so store it with the actor and keep it safe:
import { generateCryptoKeyPair } from "@fedify/fedify";
import { exportDidKey } from "@fedify/vocab-runtime";
const { publicKey, privateKey } = await generateCryptoKeyPair("Ed25519");
const did = await exportDidKey(publicKey); // did:key:z6Mk...
// The verification method that the actor's proofs refer to:
const didKeyId = new URL(`${did}#${did.slice("did:key:".length)}`);
The actor dispatcher returns a portable actor as it would an ordinary one. Context.getPortableActorUri() and its siblings for inboxes, outboxes, collections, and objects build portable IDs from the dispatcher's paths and the DID, and signObject() signs the actor with the DID's key:
federation
.setActorDispatcher("/users/{identifier}", async (ctx, identifier) => {
const user = await findUser(identifier);
if (user == null) return null;
// A request through the gateway endpoint names the DID in its path,
// e.g., GET /.well-known/apgateway/did:key:z6Mk.../users/alice:
if (
ctx.portableRequest != null &&
ctx.portableRequest.authority !== user.did
) {
return null;
}
// This server's gateway keys for the actor (see below):
const keys = await ctx.getActorKeyPairs(identifier);
return await signObject(
new Person({
id: ctx.getPortableActorUri(identifier, user.did), // ap+ef61://did:key:z6Mk.../users/alice
preferredUsername: user.username,
inbox: ctx.getPortableInboxUri(identifier, user.did),
outbox: ctx.getPortableOutboxUri(identifier, user.did),
followers: ctx.getPortableFollowersUri(identifier, user.did),
gateways: [new URL("https://example.com")],
publicKeys: keys.map((key) => key.cryptographicKey),
assertionMethods: keys.map((key) => key.multikey),
}),
user.didKeyPair.privateKey,
new URL(`${user.did}#${user.did.slice("did:key:".length)}`),
);
})
.setKeyPairsDispatcher(async (ctx, identifier) => {
const user = await findUser(identifier);
// This server's own key pair for the actor, not the DID's:
return user == null ? [] : [user.gatewayKeyPair];
})
.mapPortableActorId(async (ctx, identifier) => {
const user = await findUser(identifier);
return user == null ? null : ctx.getPortableActorUri(identifier, user.did);
});
With this dispatcher, Fedify serves the actor at its compatible identifier, https://example.com/.well-known/apgateway/did:key:z6Mk.../users/alice, which is how other servers retrieve it. It serves it through WebFinger as @alice@example.com, since the handle's domain comes from the first gateway in gateways, and at its ordinary URL as before. On the gateway endpoint, Fedify serves a document only if its ID is the requested portable ID and it carries a proof made with a key of that DID.
The key pairs dispatcher and the new ActorCallbackSetters.mapPortableActorId() give the actor gateway keys. These are the server's own keys for the actor, and they sign only the HTTP requests the server makes on the actor's behalf, such as deliveries. They never sign the actor's activities or objects; only the DID's key does that. The actor document lists them under assertionMethods, as FEP-521a describes, so that other servers can verify the HTTP Signatures. See Dispatching the actor and Gateway keys of portable actors for the details.
Object and collection dispatchers serve portable objects and collections through the gateway endpoint in the same way, routed by the path after the DID. Two rules protect the actor's data. A non-public portable object is served only if the dispatcher has an authorization predicate, because FEP-ef61 forbids a gateway to show it to anyone outside its audience; the new RequestContext.isSignedByAudience() helps to write one. Without a predicate, a request for such an object gets 404 Not Found, as though the gateway did not store it. A deleted object is served as a signed Tombstone with 410 Gone. Resources that portable objects refer to by SHA-256 hashlinks, such as GET /.well-known/apgateway/hl:zQm..., are served by the new Federatable.setHashlinkMediaDispatcher().
A portable actor's activities have to be signed by its DID, which Fedify's key pairs dispatcher does not know about. Sign them with signObject() before sending, and pass normalizeExistingProofs: true so that the activity goes out in the form its proof covers. An activity that embeds a portable object, such as the Note of a Create, is signed inside out:
const actor = withGatewayHints(
ctx.getPortableActorUri(user.identifier, user.did),
[new URL("https://example.com")],
); // ap+ef61://did:key:z6Mk.../users/alice?@gateway=https%3A%2F%2Fexample.com
const note = await signObject(
new Note({
id: ctx.getPortableObjectUri(
Note,
{ identifier: user.identifier, id: crypto.randomUUID() },
user.did,
),
attribution: actor,
to: PUBLIC_COLLECTION,
content: "Hello, world!",
}),
user.didKeyPair.privateKey,
didKeyId,
);
const create = await signObject(
new Create({
id: ctx.getPortableObjectUri(
Note,
{ identifier: user.identifier, id: crypto.randomUUID() },
user.did,
),
actor,
to: PUBLIC_COLLECTION,
object: note,
}),
user.didKeyPair.privateKey,
didKeyId,
);
await ctx.sendActivity(
{ identifier: user.identifier },
"followers",
create,
{ normalizeExistingProofs: true },
);
This example depends on a fix to signObject() that is useful outside FEP-ef61 as well. Previously, a signed object assigned to a typed parent was rebuilt under the parent's JSON-LD context when the parent was serialized. The child kept its proofValue but lost the document its proof covered, so it no longer verified on its own. signObject() now captures the exact document it signed, and nested serialization embeds that document verbatim. The captured document is a snapshot: clone() does not carry it, so sign a clone again if it has to be embedded. The design is in #1044 and the fix in #1051.
Context.sendActivity() refuses a portable activity that is unsigned, signed by the wrong key, or carries a Linked Data Signature, and throws a TypeError before anything is queued. It also refuses a few shapes of compound document that Fedify's own inboxes would reject, such as a received signed Follow embedded in an Accept; refer to such an object by its ID instead. Deliveries to portable inboxes go through the recipient's gateways, or else the @gateway hints on the inbox, trying at most five of them until one accepts. Before this release, they failed with UrlError: Unsupported protocol: ap+ef61:. The rules are in Delivering to portable actors and Producing a compound document.
On the receiving side, a portable inbox is reached as POST /.well-known/apgateway/did:key:z6Mk.../users/alice/inbox and dispatched to the same inbox listeners as the ordinary inbox. Context.parseUri() gains a portable option, and the parsed result has the DID in a new authority property to check against the one you store. Fedify accepts an activity of a portable actor, or one with a portable ID, only if it has a valid proof by that DID; neither an HTTP Signature nor a Linked Data Signature authenticates it. Each portable object embedded in the activity is verified against its own proof before dispatch. An accepted activity that carries its own proof is forwarded to the actor's other gateways at most once, as FEP-ef61 recommends. Forwarding needs a key–value store that supports cas(), and the FederationOptions.portableInboxForwarding option tunes it. See Portable inboxes and Forwarding to other gateways.
Applications that never host a portable actor still meet portable objects from other servers. Context.lookupObject() resolves portable IDs, compatible identifiers, and the fediverse handles of portable actors, and returns portable objects only if they verify:
const actor = await ctx.lookupObject("@alice@example.com");
// A portable ID without @gateway hints needs gateways to ask:
const note = await ctx.lookupObject("ap://did:key:z6Mk.../notes/1", {
gateways: ["https://example.com"],
});
Property accessors fetch portable references through gateways as well, and they verify what they fetch with the new verifyPortableObject option. Context has a matching verifyPortableObject property, so passing a context as the options, as in await create.getObject(ctx), is enough. Activities received in inboxes use this verifier by default, and objects fetched through their accessors inherit it, so a chained call such as (await create.getObject(ctx))?.getAttribution() verifies each portable object along the way. A lookup asks at most five gateways, falling back to the next when one fails or serves something that does not verify. Unsigned collections are accepted only from a gateway that the owner lists, and only for an actor's inbox, outbox, followers, following, and liked, as FEP-ef61 allows. fetchPortableMedia() retrieves the media of a portable object and returns it only if it matches the object's digestMultibase. See Dereferencing portable references and Default verifiers.
Software that does not understand ap: URIs can refer to a portable object by a compatible identifier, an HTTP(S) URL under a gateway's /.well-known/apgateway/ path such as https://gw.example/.well-known/apgateway/did:key:z6Mk.../actor. Some implementations, tootik among them, use compatible identifiers as the IDs of their portable actors.
Earlier versions of Fedify treated these URLs as ordinary web URLs and trusted them by the server that served them. Since anyone can serve a compatible identifier for any DID, https://evil.example/.well-known/apgateway/did:key:z6MkAlice/actor could act as Alice without a proof by Alice's DID. Fedify 2.4.0 treats a compatible identifier as the portable object it stands for, as FEP-ef61 requires. Inboxes reject an activity from an actor whose ID is a compatible identifier with 401 Unauthorized unless it has a valid Object Integrity Proof by the actor's DID, and there is no option to turn this off. Actors that publish compatible identifiers without signing them are no longer accepted, and neither are those whose DIDs use key types that Fedify cannot verify yet. The change was tracked in #1093 and made in #1105.
Your own portable actors may still use compatible identifiers as their IDs, for software that rejects ap: IDs. Build them with toCompatibleEf61Id() before signing, and Fedify will treat them as portable wherever it serves or sends them. It also warns if one is malformed or does not point at the actor's first gateway. See Compatible identifiers as actor IDs.
Two choices in Fedify's profile differ from the current FEP text or rest on semantics that are not settled yet. Fedify keeps both for the 2.x series, and either may change in Fedify 3.0.
The first is the scheme. FEP-ef61 recommends ap: and canonicalizes ap+ef61: to it, while also warning that the recommendation may change to ap+ef61:, since these IDs are meant only for portable objects. Fedify accepts both, compares them as equal, and serializes ap+ef61:. Parsing a document normalizes its portable IRIs to ap+ef61:, so re-serializing an object that another implementation signed with ap: IDs breaks its proof. Keep the received JSON of such documents, and compare portable IDs with arePortableUrisEqual() rather than as strings. The discussion is in #826 and #828, and the reasoning is in Canonical ap+ef61: scheme.
The second concerns compound documents. When a signed Note sits inside a signed Create, the verifier has to know which part of the JSON each proof covers, and neither FEP-8b32 nor Verifiable Credential Data Integrity defines that yet; the question is open in w3c/vc-data-integrity#350. Fedify uses an interim map-local profile, worked out in #938. Each JSON map with a proof is its own secured document, each embedded portable object needs its own proof and its own @context, and proof sets and chains are not supported. The profile authenticates the exact JSON values each proof covers; it does not promise that a child's JSON-LD expansion inside its parent matches its expansion on its own. @silverpill, @dmitrizagidulin, and @by_caballero reviewed the proposal in that thread, and @silverpill reproduced its test vectors independently. See Map-local compound proofs.
Fedify does not implement FEP-ae97, the companion proposal for client-side signing. There are no endpoints for registering actors, submitting client-signed activities to a portable outbox, or gateway discovery, and no media upload and deletion endpoints under /.well-known/apgateway-media. Your own outbox listeners can still relay activities that a client signed, unchanged. Fedify also does not copy or reconcile actors, objects, or collections between gateways, so running an actor on several gateways means keeping their data in sync yourself. There is no workflow for rotating a DID's key or migrating an actor to another DID. Fedify resolves verification methods only for did:key DIDs with Ed25519 keys; other DID methods verify only if your document loader resolves them. Gateways must be plain origins, since the arbitrary gateway paths that FEP-ef61 discusses are not supported. The full list is in Not supported.
The profile was tested against tootik v0.25.4 and Mitra v5.10.0 in #1202. Fedify verifies tootik's portable actors, activities, and HTTP Signatures, and Mitra's gateway HTTP Signatures on activities that a client signed. Both accept the portable activities that Fedify sends. Two fixes came out of this testing. Fedify now reads a gateways list that tootik leaves unmapped in its JSON-LD context. It also serializes gateways as bare origins, because Mitra rejects a trailing slash. Fedify has not yet verified an actor or activity that Mitra signed itself, and Mastodon's handling of portable actors has not been tested. The FEP-ef61 interoperability section of FEDERATION.md records what was tested and how.
fedify lookup looks up portable objects by their ap: and ap+ef61: IDs, compatible identifiers, and the handles of portable actors, and it verifies their proofs. When no gateway returns an acceptable object, it says why. A new --gateway option on fedify lookup, fedify inbox, and fedify webfinger names the gateways to ask. fedify inbox --follow follows portable actors, and fedify webfinger accepts portable actor IDs. See Looking up portable objects in the CLI manual.
The mock federation and contexts in @fedify/testing support portable IDs, verification against fixture document loaders, gateway requests, and hashlink media, so tests can exercise portable objects without a live gateway. @fedify/lint's actor URI rules accept the portable IDs that the getPortable*Uri() methods build. @fedify/next now passes hashlink media requests to Fedify, a problem reported in #1149 and fixed in #1170; to have Next.js run the middleware for them, add { source: "/.well-known/apgateway/:path*" } to your matcher.
Fedify already runs activity delivery and inbox processing on queue workers, but applications usually have jobs of their own: digest emails, timeline rebuilds, link previews. Until now, those jobs could not use Fedify's queues, serialization, or retries. Fedify 2.4.0 opens the same machinery to application-defined tasks.
A task is defined once with defineTask() and enqueued from any Context with enqueueTask() or enqueueTaskMany(). Every task takes a Standard Schema, such as a Zod or Valibot schema, which types its payload. Fedify validates the payload when it is enqueued and again when a worker picks it up; the second check catches payloads from an earlier deployment that no longer match the schema:
import { createExponentialBackoffPolicy } from "@fedify/fedify";
import { z } from "zod";
const sendDigest = federation.defineTask("sendDigest", {
schema: z.object({ userId: z.string(), since: z.date() }),
handler: async (ctx, data) => {
// data is typed as { userId: string; since: Date }
await sendEmail(data.userId, await buildDigest(data.userId, data.since));
},
retryPolicy: createExponentialBackoffPolicy({ maxAttempts: 3 }),
});
// Anywhere with a Context:
await ctx.enqueueTask(sendDigest, { userId: "alice", since: new Date() }, {
delay: { minutes: 30 },
deduplicationKey: "digest:alice",
});
Fedify serializes payloads with devalue, so Date, Map, Set, URL, bigint, Temporal values, circular references, and Activity Vocabulary objects all survive the trip through any message queue backend. A Note in a payload comes back as a Note. Failed handlers are retried with exponential backoff by default, and queues with native retries handle retries themselves. Tasks fall back to the outbox queue unless you give them their own. Use the new task slot of the queue option or a per-task queue for that, and startQueue(ctxData, { queue: "task" }) runs a task-only worker. Setting taskQueueResolution: "strict" turns the fallback into an error.
A deduplicationKey deduplicates enqueues. The feature came from @julian's point in #206 that recurring tasks are hard to manage when you cannot tell whether one is already queued. A queue that declares nativeDeduplication enforces the key itself; otherwise, Fedify keeps a best-effort marker in the key–value store through cas(). Each task runs in a fedify.task span, and the queue metrics from 2.3.0 now include task queues. The Background tasks chapter covers routing, retries, deduplication, and the limitations; cron-style scheduling, result backends, and priorities are out of scope for now.
ChanHaeng Lee (@2chanhaeng) implemented the API: the core in #803, deduplication in #806, and observability in #812, following the plans in #797, #798, and #799. The work was merged in #923, closing #206, which had been open since February 2025.
Fedify 2.2.0 added client-to-server outbox listeners, but a C2S client also has to upload images and video before it can post them. That traffic does not go through POST /outbox. Fedify 2.4.0 supports the ActivityPub Media Upload extension, a SocialCG proposal last updated in 2017 that never reached W3C Recommendation status.
setMediaUploader() registers a multipart/form-data endpoint. Its callback receives the uploaded file and the object shell the client posted along with it. If the callback returns the created object, Fedify responds with 201 Created; if it returns a URL where the object will appear once processing finishes, Fedify responds with 202 Accepted:
federation
.setMediaUploader(
"/users/{identifier}/media",
async (ctx, identifier, file, object) => {
const stored = await uploadToStorage(file);
return new Image({
id: ctx.getObjectUri(Image, { uuid: stored.uuid }),
url: new URL(stored.publicUrl),
mediaType: file.type,
name: object.name,
});
},
)
.authorize(async (ctx, identifier) => {
const session = await verifyAccessToken(
ctx.request.headers.get("authorization"),
);
return session?.identifier === identifier;
});
The actor dispatcher advertises the endpoint itself by setting endpoints: new Endpoints({ uploadMedia: ctx.getMediaUploaderUri(identifier) }), the same way it places its inbox and outbox URIs. Without an authorize() hook, anyone who finds the endpoint can upload to it. For that reason, Fedify logs a warning when an uploader is registered without one, and when the returned object's URI does not match an object dispatcher or the actor does not advertise the endpoint. Four new @fedify/lint rules catch the same mistakes before they run. Fedify reads the upload into memory and sets no size limit of its own, so cap the request size at your reverse proxy. Storing the file and serving the object back are up to the application. See the Media upload chapter; the work is in #754 and #927.
Fedify decides several things about every inbox delivery: which signature mechanisms applied, which keys they tried, whether the actor owned the key, and what happened to the activity. Before this release, applications saw only part of that. A Linked Data Signature by a key the actor did not own was counted the same as a bad signature, and the key that verified a request was never handed to the application. The request in #1191 came from an effort to log these decisions for a debugging tool.
The new onRequestFinished() callback on inbox listeners receives an InboxRequestReport for every delivery to an inbox route, including rejected requests and requests that fail before verification:
federation
.setInboxListeners("/users/{identifier}/inbox", "/inbox")
.on(Follow, async (ctx, follow) => { /* ... */ })
.onRequestFinished(async (ctx, report) => {
await db.insertInboxLog({
inbox: report.inbox.kind, // "personal" | "shared" | "portable"
authentication: report.authentication.status,
outcome: report.outcome.type === "response"
? report.outcome.disposition // "processed" | "enqueued" | "duplicate" | ...
: `exception:${report.outcome.stage}`,
keyIds: report.attempts
.flatMap((attempt) => attempt.checks)
.flatMap((check) => check.triedKeys)
.map((key) => key.id?.href),
});
});
Each HTTP Signature, Linked Data Signature, or Object Integrity Proof check appears as an attempt with the actual keys it tried, including a stale cached key and its refreshed replacement. The report also carries the final authentication decision, such as actorKeyMismatch or proofPolicy, and the response or exception that ended the request. Fedify awaits the callback whether or not the trace is sampled. Errors it throws are logged without changing the response, and enqueued means the queue accepted the activity, not that a worker later processed it. Mock federations in @fedify/testing accept the same callback. See Observing inbox requests; the implementation is in #1201.
Actor dispatchers have been able to return a Tombstone since 2.2.0, but object dispatchers could only return null. A deleted post was answered with 404 Not Found, as if it had never existed, so applications that wanted 410 Gone, as ActivityPub recommends, needed their own route in front of Fedify.
Object dispatchers may now return a Tombstone, which Fedify serves with 410 Gone and the serialized tombstone after applying the authorization predicate:
federation.setObjectDispatcher(
Note,
"/users/{identifier}/notes/{id}",
async (ctx, values) => {
const post = await getPost(values.identifier, values.id);
if (post == null) return null;
if (post.deletedAt != null) {
return new Tombstone({
id: ctx.getObjectUri(Note, values),
formerType: Note,
deleted: post.deletedAt,
});
}
return new Note({ id: ctx.getObjectUri(Note, values), content: post.content });
},
);
The return type of ObjectDispatcher widens to TObject | Tombstone | null. RequestContext.getObject() still returns null for a tombstone unless you pass { tombstone: "passthrough" } or ask for the Tombstone or Object class itself. Dispatchers registered for Tombstone or Object that already returned tombstones now respond with 410 rather than 200. See Deleted objects; the change was proposed in #1112 and made in #1117.
Much of what Fedify fetches is chosen by the sender of an incoming request. Key IDs in signatures are one example, and the servers behind them can be slow on purpose. Several changes in 2.4.0 bound that work.
As #1130 points out, a request may carry several RFC 9421 signatures, and each one can make Fedify fetch a key from a URL of the sender's choosing. Fedify now verifies at most the first three, in Signature-Input order, and ignores the rest. A signature counts toward the limit even if it fails before its key is fetched, and a key named by several signatures is looked up once. The new FederationOptions.maxHttpSignatures option, added with the limit in #1166, changes it.
The built-in document loaders now give each call ten seconds by default, a limit proposed in #1131 and added in #1169. The time limit covers every redirect, alternate document, double-knocking retry, and the response body. A timed-out call throws a FetchError whose cause is a DOMException named "TimeoutError". Error responses are read in full before the loader throws, up to 1 MiB, so that a server cannot send a 404 header and then stall the body. FederationOptions.documentLoaderTimeout changes the limit or turns it off with null, and custom document loaders are not affected. Since #913, getDocumentLoader() also rejects HTML responses that do not advertise an ActivityPub alternate with a FetchError, rather than failing inside JSON.parse(), which #912 found cluttering error trackers.
Cached public keys and remembered HTTP Message Signatures specs used to live forever in the key–value store. #1017 reports a production Redis instance holding 194,424 public-key entries, about 165 MiB, for actors that had mostly stopped federating. Keys now expire after 30 days and specs after 90, configurable through FederationOptions.publicKeyTtl and FederationOptions.httpMessageSignaturesSpecTtl. Entries written by earlier versions have no expiry until they are next written. The Clearing legacy cache entries section shows how to remove them for Redis, PostgreSQL, and other stores. Heewon Chae (@heeoneie) implemented this in #1027, their first contribution to Fedify.
const federation = createFederation<void>({
kv,
maxHttpSignatures: 1, // default: 3
documentLoaderTimeout: { seconds: 5 }, // default: 10 seconds
publicKeyTtl: { days: 7 }, // default: 30 days
httpMessageSignaturesSpecTtl: { days: 30 }, // default: 90 days
});
Finally, verifyProof() now authenticates every option of an Ed25519 JCS proof except proofValue, including expires, domain, challenge, and nonce. It also rejects expired or malformed ones. Previously, a field such as expires could be added to an already signed proof without invalidating it. Jiwon Kwon (@z9mb1) spotted this in review of #968. Callers can now require a domain and challenge through VerifyProofOptions to prevent cross-domain or replayed use.
@fedify/adonisjs@fedify/adonisjs integrates Fedify with AdonisJS. Running node ace add @fedify/adonisjs registers a service provider that owns the federation's lifecycle and a server middleware that mounts it. It also scaffolds config/fedify.ts, start/federation.ts, and app/federation/main.ts. Controllers get a ctx.federation request context:
// app/federation/main.ts
import federation from "@fedify/adonisjs/services/builder";
import { Person } from "@fedify/vocab";
federation.setActorDispatcher("/actors/{identifier}", (ctx, identifier) =>
new Person({ id: ctx.getActorUri(identifier), preferredUsername: identifier }));
The middleware sits first in AdonisJS's server stack rather than its router stack. That way Fedify sees paths the router does not know, such as /.well-known/webfinger, and reads request bodies before the body parser consumes them. The package targets Node.js and is published to npm only. See the AdonisJS section of the integration guide.
Samuel Brinkmann (@sabrinkmann) contributed the package in #1006, closing #139, and built it on a prototype that Emelia Smith (@thisismissem) attached to that issue.
npm add @fedify/adonisjs
pnpm add @fedify/adonisjs
yarn add @fedify/adonisjs
@fedify/interaction-controlsGoToSocial interaction controls, FEP-044f quote authorization, and FEP-7aa9 featured collections share a request-and-authorization flow. A server asks to reply to, quote, or feature a post, and the author's server approves according to the post's interactionPolicy. @fedify/interaction-controls implements that flow for like, reply, announce, quote, and feature interactions. Each has a helper, such as likeInteraction or quoteInteraction, that creates and verifies requests and authorizations, evaluates policies, and recognizes interactions that arrived without a request:
import { likeInteraction } from "@fedify/interaction-controls";
// When a LikeRequest arrives:
const verified = await likeInteraction.verifyRequest(ctx, { request });
if (verified.verified) {
const decision = await likeInteraction.evaluatePolicy(ctx, {
subject: note,
requester: request.actorId!,
});
if (decision.result === "automatic") { // or "manual" or "denied"
const authorization = likeInteraction.createAuthorization({
id: new URL("https://example.com/authorizations/1"),
attributedTo: owner,
interactingObject: like,
interactionTarget: note,
});
// ...
}
}
The package does not install inbox listeners or store anything; the application decides where requests and authorizations live and when a human needs to approve. Verification fails closed. Without a policy, likes, replies, and announces are approved automatically, while quotes and features are denied. @fedify/vocab gains the FEP-7aa9 types this needs: FeaturedCollection, FeaturedItem, FeatureRequest, and FeatureAuthorization, along with actors' featuredCollections and InteractionPolicy.canFeature. Samuel Brinkmann asked for them in #810 once Mastodon 4.6 shipped the FEP. See the Interaction controls chapter; the work is in #914 and #929.
npm add @fedify/interaction-controls
pnpm add @fedify/interaction-controls
yarn add @fedify/interaction-controls
deno add jsr:@fedify/interaction-controls
bun add @fedify/interaction-controls
@fedify/netlify@fedify/netlify lets a Fedify application run on Netlify Functions without an external database or queue. NetlifyMessageQueue submits jobs as Netlify Async Workloads events, and createNetlifyQueueHandler() turns a Function into their consumer, with delayed delivery, native retries, and per-key FIFO ordering. NetlifyBlobsKvStore is a key–value store on Netlify Blobs with expiration, prefix listing, and compare-and-swap:
import {
createNetlifyQueueHandler,
NetlifyBlobsKvStore,
NetlifyMessageQueue,
} from "@fedify/netlify";
import { AsyncWorkloadsClient } from "@netlify/async-workloads";
import { getStore } from "@netlify/blobs";
export const kv = new NetlifyBlobsKvStore(
getStore({ name: "fedify", consistency: "strong" }),
);
export const queue = new NetlifyMessageQueue({
client: new AsyncWorkloadsClient(),
orderingKv: kv,
});
// netlify/functions/fedify-queue.ts
export default createNetlifyQueueHandler({
queue,
maxRetries: 4,
federation: () => builder.build({ kv, queue, manuallyStartQueue: true }),
});
The key–value store exists because of cost. #1010 describes a static blog whose ActivityPub traffic kept a Netlify Database from ever idling, which used most of the account's monthly credits. Jiwon Kwon contributed NetlifyBlobsKvStore in #1029, and the queue landed in #934. See Netlify Functions in the deployment guide.
npm add @fedify/netlify
pnpm add @fedify/netlify
yarn add @fedify/netlify
deno add jsr:@fedify/netlify
bun add @fedify/netlify
@fedify/pglite@fedify/pglite provides PgliteKvStore, a KvStore on an embedded PGlite database that you create and pass in. It uses the same schema as PostgresKvStore. It is meant for a single PGlite instance in a single runtime isolate, so it does not include a message queue: PGlite does not share data between processes.
import { PGlite } from "@electric-sql/pglite";
import { PgliteKvStore } from "@fedify/pglite";
const federation = createFederation({
kv: new PgliteKvStore(new PGlite("./data/fedify")),
});
ChanHaeng Lee contributed the package in #1020, together with testKvStore() in @fedify/testing. testKvStore() is a conformance suite for KvStore implementations, the counterpart to testMessageQueue(). See PgliteKvStore.
npm add @fedify/pglite
pnpm add @fedify/pglite
yarn add @fedify/pglite
deno add jsr:@fedify/pglite
bun add @fedify/pglite
Portable inbox forwarding, task deduplication, Netlify queue ordering, and the delivery circuit breaker all need an atomic compare-and-swap on the key–value store. PostgresKvStore now implements cas(), added in #934, and so does RedisKvStore, requested in #1163 and added in #1167. The Redis version uses a single-key Lua script, which works on standalone Redis and Redis Cluster. Before this, the circuit breaker fell back to racy updates on Redis.
Both come with conditions. RedisKvStore needs Redis to permit EVAL, and custom codecs must encode equal values identically. PostgresKvStore now creates logged tables by default, because losing sequence state in crash recovery could block queued events for good. It migrates existing unlogged tables when it initializes. That one-time migration rewrites the table and locks it exclusively, so schedule the upgrade accordingly if the table is large or busy; unlogged: true keeps the old behavior.
fedify init and the CLIfedify init can now scaffold a SvelteKit project, contributed by Jang Hanarae (@menele) in #971. Jang Hanarae also added a test task to generated projects in #990. It starts the app, waits for it to be ready, and checks that it resolves a local actor, which gives new projects a smoke test to run after scaffolding and after later changes. Before generating anything, fedify init now checks that the selected Deno, Bun, or Node.js meets Fedify's minimum version, or a framework's higher one, such as Astro's Node.js 22.12. It fails with a clear error in non-interactive mode, and in interactive mode it disables the affected package managers. Lee Jeongmin (@userjmmm) contributed the check in #981. Astro projects now use Astro 7, and since #936 @fedify/astro is tested against Astro 5, 6, and 7 with real builds on Node.js, Deno, and Bun, as #931 asked.
fedify tunnel gains fedify.com.es, a tunneling service run by the Fedify project that needs no account; the CLI pins its SSH host key and refuses a mismatched server before exposing a local port. localhost.run is removed in the same change, #940, because the service no longer exists. fedify lookup now times out each request after ten seconds when -T/--timeout is not given, and the option now also bounds the wait for a DNS lookup. The CLI moves to Optique 1.3.2 in #1197, which handles attached values like --timeout=30 consistently, gives better suggestions for mistyped options, and no longer hides errors for known options behind positional arguments before --.
@fedify/lint gains several rules beyond the media upload ones mentioned above. actor-preferred-username-required, requested in #895 and added in #1022, warns when an actor dispatcher returns an actor without preferredUsername. outbox-listener-delivery-not-awaited, from #1057 and #1067, reports an outbox listener that calls ctx.sendActivity() or ctx.forwardActivity() and drops the promise. On Cloudflare Workers, such an activity may never leave, because pending work is discarded once the response returns. await, return, Promise.all(), Promise.allSettled(), and waitUntil() count as handling the promise, while void, Promise.race(), and Promise.any() are accepted as a deliberate choice not to wait. The rule is a warning in the ESLint recommended configuration and an error in strict. Oxlint users enable it by name, and it is not available in Deno Lint.
In #1050, which resolves #900, the existing outbox-listener-delivery-required rule began following control flow instead of scanning the listener's source as text. It reports a listener whose only delivery calls sit in a dead branch, after an unconditional return or throw, or in a function that is never used, and it stays quiet when it cannot tell. Jae-Hyuk-Jang (@jhyuk) contributed this change and both new rules. archie (@ArchieTansaria) then taught both delivery rules about statically false loops and loop binding patterns in #1088. See the linting guide.
@fedify/vocab supports the FEP-22cd draft for translations. A Translation records a translated version's language, translators, source object, and source review time, and the new Object.translations property attaches them to a post without creating separate posts. See Translation metadata. (#1037, #1038)@fedify/vocab-tools gains an extraContext property option, which adds a JSON-LD context only when its terms are used, and a trustEmbeddedObjects type option. (#1037, #1038)suppressError: true handles are now logged as warnings rather than errors, so that they no longer show up as application errors. Contributed by Jae Hui Hong (@axz0612). (#933, #1035)enqueueMany() makes separate sends can set MessageQueue.atomicEnqueueMany to false. Fedify then rejects a multi-message batch governed by one deduplicationKey up front, since a partial send followed by a retry could enqueue duplicates. (#930, #934)Temporal bundle temporal-polyfill instead of requiring @js-temporal/polyfill at runtime, and type declarations rely on the standard esnext.temporal lib. (#823, #925)Accept header now includes the ActivityStreams profile on the JSON-LD media type, as ActivityPub and FEP-ef61 gateways require. (#834, #1077)fedify lookup --recurse reporting a timeout or other network failure as a possibly private object; it now reports the actual cause. (#1156, #1199)-p/--allow-private-address option of fedify webfinger being ignored. (#1156, #1199)outbox-listener-delivery-required and outbox-listener-delivery-not-awaited losing track of an object's functions when a nested helper assigns another property of that object. (#1125, #1140)npm add @fedify/fedify@2.4.0
pnpm add @fedify/fedify@2.4.0
yarn add @fedify/fedify@2.4.0
deno add jsr:@fedify/fedify@2.4.0
bun add @fedify/fedify@2.4.0
Update the other @fedify/* packages you use to 2.4.0 at the same time. To update the CLI, run npm install -g @fedify/cli@2.4.0, or see the installation instructions for other package managers.
Most applications need no code changes, but these changes in behavior may affect you:
401 Unauthorized unless they carry a valid Object Integrity Proof by the actor's DID. Keys at compatible identifiers, and keys at ordinary URLs that claim a portable owner, are no longer trusted by origin.Context as their options, such as await create.getObject(ctx), now verify the portable objects they dereference, including references to compatible identifiers from objects with ordinary HTTP(S) IDs./.well-known/apgateway/, and Context.sendActivity() may reject an activity that embeds others' portable objects. Next.js applications need the matcher shown above to serve hashlink media.maxHttpSignatures if you need more.fedify lookup does too.Tombstone or Object that return tombstones now respond with 410 Gone instead of 200 OK.PostgresKvStore migrates existing unlogged tables to logged tables on initialization, which locks the table.RedisKvStore needs EVAL permission for cas().Context and RequestContext interfaces need the new getPortable*Uri() methods and isSignedByAudience().fedify tunnel no longer supports localhost.run.Thanks to this release's contributors:
@fedify/adonisjs, and the request for FEP-7aa9 vocabularytest task in fedify initfedify initactor-preferred-username-required and outbox-listener-delivery-not-awaited lint rules, and control-flow analysis for outbox-listener-delivery-requiredChanHaeng Lee (@2chanhaeng) and Jiwon Kwon (@z9mb1) joined as maintainers after 2.3.0. ChanHaeng built the background task API and @fedify/pglite, Jiwon built NetlifyBlobsKvStore and rewrote the relay documentation, and both reviewed much of the rest of this release.
Many people added tests, documentation, and infrastructure during this cycle, often starting from a good first issue: Seoa Yoon (@bananamilk452), Lim Kyoujin (@dktsudgg), @m2nsp, Cho Seongmin (@seongmin36), Safal Shrestha (@Safal-Shrestha-SS), Jo Youngjae (@jaonz6057), @kecan0406, Loi Nguyen (@lntutor), Oh Taejun (@0w0), Yerin Park (@dstforye), Lee Dogeon (@moreal), and Sae Jin Kim (@siliconsjang).
The FEP-ef61 work owes a lot to people outside the project. @anotherdoesnm opened the request in #288. @silverpill, the author of FEP-ef61 and FEP-ae97, clarified the gateway trust model, reviewed the compound proof profile, and checked its test vectors. @dmitrizagidulin and @by_caballero weighed in on the profile's JSON-LD questions in #938. Testing against tootik and Mitra led to two of the interoperability fixes in this release.
See CHANGES.md for the complete changelog.
github.com
ActivityPub server framework in TypeScript. Contribute to fedify-dev/fedify development by creating an account on GitHub.
30 Sep 2026
Activity Relay 3.0.1 has been released to fix a couple bugs caused by invalid follower states blocking relay operation.
https://github.com/thystra/Activity-Relay/releases/tag/v3.0.1
Activity-Relay 3.0.1 is a focused reliability release for follower-state handling and relay fan-out.
No configuration or data migration is required.
The 3.0.1 candidate was production-tested on relay.argentwolf.org, including real WordPress fan-out to healthy Friendica and Mastodon receivers while an independently failing receiver continued through its isolated retry path.
Container: ghcr.io/thystra/activity-relay:3.0.1
See docs/releases/v3.0.1.md and CHANGELOG.md for complete details.
The attached files are the exact canonical artifacts produced and accepted by the Forgejo release workflow.
If you find this useful, please consider leaving a tip on my Ko-Fi – https://ko-fi.com/thewolfandtheraven
#activitypub #fediverse #friendica #mastodon #nodebb #relay #relays #relayserver
30 Sep 2026
Activity Relay 3.0.1 has been released to fix a couple bugs caused by invalid follower states blocking relay operation.
https://github.com/thystra/Activity-Relay/releases/tag/v3.0.1
Activity-Relay 3.0.1 is a focused reliability release for follower-state handling and relay fan-out.
No configuration or data migration is required.
The 3.0.1 candidate was production-tested on relay.argentwolf.org, including real WordPress fan-out to healthy Friendica and Mastodon receivers while an independently failing receiver continued through its isolated retry path.
Container: ghcr.io/thystra/activity-relay:3.0.1
See docs/releases/v3.0.1.md and CHANGELOG.md for complete details.
The attached files are the exact canonical artifacts produced and accepted by the Forgejo release workflow.
If you find this useful, please consider leaving a tip on my Ko-Fi – https://ko-fi.com/thewolfandtheraven
#activitypub #fediverse #friendica #mastodon #nodebb #relay #relays #relayserver
Why could this be happening? Mastodon Web - Advanced interface?
Nothing has changed on my side. This is from my bot: @adventuregames
30 Sep 2026
Activity Relay 3.0.1 has been released to fix a couple bugs caused by invalid follower states blocking relay operation.
https://github.com/thystra/Activity-Relay/releases/tag/v3.0.1
Activity-Relay 3.0.1 is a focused reliability release for follower-state handling and relay fan-out.
No configuration or data migration is required.
The 3.0.1 candidate was production-tested on relay.argentwolf.org, including real WordPress fan-out to healthy Friendica and Mastodon receivers while an independently failing receiver continued through its isolated retry path.
Container: ghcr.io/thystra/activity-relay:3.0.1
See docs/releases/v3.0.1.md and CHANGELOG.md for complete details.
The attached files are the exact canonical artifacts produced and accepted by the Forgejo release workflow.
If you find this useful, please consider leaving a tip on my Ko-Fi – https://ko-fi.com/thewolfandtheraven
#activitypub #fediverse #friendica #mastodon #nodebb #relay #relays #relayserver
Fedify 2.4.0 is out.
The main addition is support for FEP-ef61 portable objects. A Fedify server can now host actors whose identity is a DID instead of a domain, serve their signed objects through /.well-known/apgateway/, and verify the portable objects of other servers. I tested it against tootik and Mitra, and wrote up where Fedify's profile differs from the FEP and what it leaves out.
The release also adds application-defined background tasks, the ActivityPub Media Upload extension, per-request inbox reports, 410 Gone for deleted objects, and four new packages (AdonisJS, Netlify, PGlite, and interaction controls).
Some defaults changed, so please read the upgrading section before you update, especially if you federate with software that uses FEP-ef61 compatible identifiers.
Release notes: https://hackers.pub/@fedify/2026/fedify-2-4.
Thanks to everyone who contributed to this cycle, and to @silverpill for patient answers about the FEP.
hackers.pub
Fedify is a TypeScript framework for building ActivityPub servers. It implements federation details such as HTTP Signatures, JSON-LD processing, WebFinger, inbox and outbox routing, and activity delivery.
Fedify 2.4.0 is a minor release with an unusually long changelog. The main addition is support for FEP-ef61 portable objects: a Fedify server can now host actors whose identity is a DID rather than a domain, serve and verify their signed objects, and consume the portable objects of other servers. The release also adds an API for application-defined background tasks, the ActivityPub Media Upload extension, a hook that reports how every inbox request was authenticated and handled, and 410 Gone tombstones for deleted objects. Several defaults change to limit what a hostile peer can make Fedify do. Four new packages ship with this version: @fedify/adonisjs, @fedify/interaction-controls, @fedify/netlify, and @fedify/pglite.
Most applications can upgrade without code changes, but a few of the new defaults are behavior changes. They are listed in the Upgrading section at the end.
An ActivityPub object's ID is an HTTP(S) URL on its server's domain. If that server shuts down, its IDs stop resolving, and an actor that moves elsewhere gets a new ID that everyone has to follow again. FEP-ef61, written by @silverpill of Mitra, proposes IDs that do not name a server at all. A portable ID is an ap: URI whose authority is a DID:
ap://did:key:z6MkrJVnaZkeFzdQyMZu1cgjg7k1pZZ6pvBQ7XJPt4swbTQ2/actor
The same object can be stored on several servers, called gateways, each of which serves it under /.well-known/apgateway/. Because no server owns the ID, a portable actor, activity, or object is authenticated only by its FEP-8b32 Object Integrity Proof, made with a key of the DID in its ID. A portable actor lists its gateways in a gateways property, and other servers deliver to its inbox through them.
Fedify 2.4.0 implements a profile of FEP-ef61 for gateways and consumers of portable objects. It makes two deliberate choices where the FEP or the specifications it rests on leave questions open, and it leaves out a few parts of the proposal; both are described below. The new Portable objects chapter of the manual walks through running portable actors from start to finish and then describes the profile as a whole, so that other implementers can tell what to expect from a Fedify server. The request came from @anotherdoesnm in #288, which tracked the work from a roadmap to the final pieces.
Portable objects are a foundation for nomadic identity, not nomadic identity itself. Fedify serves and verifies portable objects and forwards activities between gateways, but it does not copy actors or posts between servers, rotate keys, or move an actor to another DID. Full nomadic identity is still open in #413.
A portable actor starts with an Ed25519 key pair. Its DID is made from the public key with exportDidKey(), and whoever holds the private key can act as the actor, so store it with the actor and keep it safe:
import { generateCryptoKeyPair } from "@fedify/fedify";
import { exportDidKey } from "@fedify/vocab-runtime";
const { publicKey, privateKey } = await generateCryptoKeyPair("Ed25519");
const did = await exportDidKey(publicKey); // did:key:z6Mk...
// The verification method that the actor's proofs refer to:
const didKeyId = new URL(`${did}#${did.slice("did:key:".length)}`);
The actor dispatcher returns a portable actor as it would an ordinary one. Context.getPortableActorUri() and its siblings for inboxes, outboxes, collections, and objects build portable IDs from the dispatcher's paths and the DID, and signObject() signs the actor with the DID's key:
federation
.setActorDispatcher("/users/{identifier}", async (ctx, identifier) => {
const user = await findUser(identifier);
if (user == null) return null;
// A request through the gateway endpoint names the DID in its path,
// e.g., GET /.well-known/apgateway/did:key:z6Mk.../users/alice:
if (
ctx.portableRequest != null &&
ctx.portableRequest.authority !== user.did
) {
return null;
}
// This server's gateway keys for the actor (see below):
const keys = await ctx.getActorKeyPairs(identifier);
return await signObject(
new Person({
id: ctx.getPortableActorUri(identifier, user.did), // ap+ef61://did:key:z6Mk.../users/alice
preferredUsername: user.username,
inbox: ctx.getPortableInboxUri(identifier, user.did),
outbox: ctx.getPortableOutboxUri(identifier, user.did),
followers: ctx.getPortableFollowersUri(identifier, user.did),
gateways: [new URL("https://example.com")],
publicKeys: keys.map((key) => key.cryptographicKey),
assertionMethods: keys.map((key) => key.multikey),
}),
user.didKeyPair.privateKey,
new URL(`${user.did}#${user.did.slice("did:key:".length)}`),
);
})
.setKeyPairsDispatcher(async (ctx, identifier) => {
const user = await findUser(identifier);
// This server's own key pair for the actor, not the DID's:
return user == null ? [] : [user.gatewayKeyPair];
})
.mapPortableActorId(async (ctx, identifier) => {
const user = await findUser(identifier);
return user == null ? null : ctx.getPortableActorUri(identifier, user.did);
});
With this dispatcher, Fedify serves the actor at its compatible identifier, https://example.com/.well-known/apgateway/did:key:z6Mk.../users/alice, which is how other servers retrieve it. It serves it through WebFinger as @alice@example.com, since the handle's domain comes from the first gateway in gateways, and at its ordinary URL as before. On the gateway endpoint, Fedify serves a document only if its ID is the requested portable ID and it carries a proof made with a key of that DID.
The key pairs dispatcher and the new ActorCallbackSetters.mapPortableActorId() give the actor gateway keys. These are the server's own keys for the actor, and they sign only the HTTP requests the server makes on the actor's behalf, such as deliveries. They never sign the actor's activities or objects; only the DID's key does that. The actor document lists them under assertionMethods, as FEP-521a describes, so that other servers can verify the HTTP Signatures. See Dispatching the actor and Gateway keys of portable actors for the details.
Object and collection dispatchers serve portable objects and collections through the gateway endpoint in the same way, routed by the path after the DID. Two rules protect the actor's data. A non-public portable object is served only if the dispatcher has an authorization predicate, because FEP-ef61 forbids a gateway to show it to anyone outside its audience; the new RequestContext.isSignedByAudience() helps to write one. Without a predicate, a request for such an object gets 404 Not Found, as though the gateway did not store it. A deleted object is served as a signed Tombstone with 410 Gone. Resources that portable objects refer to by SHA-256 hashlinks, such as GET /.well-known/apgateway/hl:zQm..., are served by the new Federatable.setHashlinkMediaDispatcher().
A portable actor's activities have to be signed by its DID, which Fedify's key pairs dispatcher does not know about. Sign them with signObject() before sending, and pass normalizeExistingProofs: true so that the activity goes out in the form its proof covers. An activity that embeds a portable object, such as the Note of a Create, is signed inside out:
const actor = withGatewayHints(
ctx.getPortableActorUri(user.identifier, user.did),
[new URL("https://example.com")],
); // ap+ef61://did:key:z6Mk.../users/alice?@gateway=https%3A%2F%2Fexample.com
const note = await signObject(
new Note({
id: ctx.getPortableObjectUri(
Note,
{ identifier: user.identifier, id: crypto.randomUUID() },
user.did,
),
attribution: actor,
to: PUBLIC_COLLECTION,
content: "Hello, world!",
}),
user.didKeyPair.privateKey,
didKeyId,
);
const create = await signObject(
new Create({
id: ctx.getPortableObjectUri(
Note,
{ identifier: user.identifier, id: crypto.randomUUID() },
user.did,
),
actor,
to: PUBLIC_COLLECTION,
object: note,
}),
user.didKeyPair.privateKey,
didKeyId,
);
await ctx.sendActivity(
{ identifier: user.identifier },
"followers",
create,
{ normalizeExistingProofs: true },
);
This example depends on a fix to signObject() that is useful outside FEP-ef61 as well. Previously, a signed object assigned to a typed parent was rebuilt under the parent's JSON-LD context when the parent was serialized. The child kept its proofValue but lost the document its proof covered, so it no longer verified on its own. signObject() now captures the exact document it signed, and nested serialization embeds that document verbatim. The captured document is a snapshot: clone() does not carry it, so sign a clone again if it has to be embedded. The design is in #1044 and the fix in #1051.
Context.sendActivity() refuses a portable activity that is unsigned, signed by the wrong key, or carries a Linked Data Signature, and throws a TypeError before anything is queued. It also refuses a few shapes of compound document that Fedify's own inboxes would reject, such as a received signed Follow embedded in an Accept; refer to such an object by its ID instead. Deliveries to portable inboxes go through the recipient's gateways, or else the @gateway hints on the inbox, trying at most five of them until one accepts. Before this release, they failed with UrlError: Unsupported protocol: ap+ef61:. The rules are in Delivering to portable actors and Producing a compound document.
On the receiving side, a portable inbox is reached as POST /.well-known/apgateway/did:key:z6Mk.../users/alice/inbox and dispatched to the same inbox listeners as the ordinary inbox. Context.parseUri() gains a portable option, and the parsed result has the DID in a new authority property to check against the one you store. Fedify accepts an activity of a portable actor, or one with a portable ID, only if it has a valid proof by that DID; neither an HTTP Signature nor a Linked Data Signature authenticates it. Each portable object embedded in the activity is verified against its own proof before dispatch. An accepted activity that carries its own proof is forwarded to the actor's other gateways at most once, as FEP-ef61 recommends. Forwarding needs a key–value store that supports cas(), and the FederationOptions.portableInboxForwarding option tunes it. See Portable inboxes and Forwarding to other gateways.
Applications that never host a portable actor still meet portable objects from other servers. Context.lookupObject() resolves portable IDs, compatible identifiers, and the fediverse handles of portable actors, and returns portable objects only if they verify:
const actor = await ctx.lookupObject("@alice@example.com");
// A portable ID without @gateway hints needs gateways to ask:
const note = await ctx.lookupObject("ap://did:key:z6Mk.../notes/1", {
gateways: ["https://example.com"],
});
Property accessors fetch portable references through gateways as well, and they verify what they fetch with the new verifyPortableObject option. Context has a matching verifyPortableObject property, so passing a context as the options, as in await create.getObject(ctx), is enough. Activities received in inboxes use this verifier by default, and objects fetched through their accessors inherit it, so a chained call such as (await create.getObject(ctx))?.getAttribution() verifies each portable object along the way. A lookup asks at most five gateways, falling back to the next when one fails or serves something that does not verify. Unsigned collections are accepted only from a gateway that the owner lists, and only for an actor's inbox, outbox, followers, following, and liked, as FEP-ef61 allows. fetchPortableMedia() retrieves the media of a portable object and returns it only if it matches the object's digestMultibase. See Dereferencing portable references and Default verifiers.
Software that does not understand ap: URIs can refer to a portable object by a compatible identifier, an HTTP(S) URL under a gateway's /.well-known/apgateway/ path such as https://gw.example/.well-known/apgateway/did:key:z6Mk.../actor. Some implementations, tootik among them, use compatible identifiers as the IDs of their portable actors.
Earlier versions of Fedify treated these URLs as ordinary web URLs and trusted them by the server that served them. Since anyone can serve a compatible identifier for any DID, https://evil.example/.well-known/apgateway/did:key:z6MkAlice/actor could act as Alice without a proof by Alice's DID. Fedify 2.4.0 treats a compatible identifier as the portable object it stands for, as FEP-ef61 requires. Inboxes reject an activity from an actor whose ID is a compatible identifier with 401 Unauthorized unless it has a valid Object Integrity Proof by the actor's DID, and there is no option to turn this off. Actors that publish compatible identifiers without signing them are no longer accepted, and neither are those whose DIDs use key types that Fedify cannot verify yet. The change was tracked in #1093 and made in #1105.
Your own portable actors may still use compatible identifiers as their IDs, for software that rejects ap: IDs. Build them with toCompatibleEf61Id() before signing, and Fedify will treat them as portable wherever it serves or sends them. It also warns if one is malformed or does not point at the actor's first gateway. See Compatible identifiers as actor IDs.
Two choices in Fedify's profile differ from the current FEP text or rest on semantics that are not settled yet. Fedify keeps both for the 2.x series, and either may change in Fedify 3.0.
The first is the scheme. FEP-ef61 recommends ap: and canonicalizes ap+ef61: to it, while also warning that the recommendation may change to ap+ef61:, since these IDs are meant only for portable objects. Fedify accepts both, compares them as equal, and serializes ap+ef61:. Parsing a document normalizes its portable IRIs to ap+ef61:, so re-serializing an object that another implementation signed with ap: IDs breaks its proof. Keep the received JSON of such documents, and compare portable IDs with arePortableUrisEqual() rather than as strings. The discussion is in #826 and #828, and the reasoning is in Canonical ap+ef61: scheme.
The second concerns compound documents. When a signed Note sits inside a signed Create, the verifier has to know which part of the JSON each proof covers, and neither FEP-8b32 nor Verifiable Credential Data Integrity defines that yet; the question is open in w3c/vc-data-integrity#350. Fedify uses an interim map-local profile, worked out in #938. Each JSON map with a proof is its own secured document, each embedded portable object needs its own proof and its own @context, and proof sets and chains are not supported. The profile authenticates the exact JSON values each proof covers; it does not promise that a child's JSON-LD expansion inside its parent matches its expansion on its own. @silverpill, @dmitrizagidulin, and @by_caballero reviewed the proposal in that thread, and @silverpill reproduced its test vectors independently. See Map-local compound proofs.
Fedify does not implement FEP-ae97, the companion proposal for client-side signing. There are no endpoints for registering actors, submitting client-signed activities to a portable outbox, or gateway discovery, and no media upload and deletion endpoints under /.well-known/apgateway-media. Your own outbox listeners can still relay activities that a client signed, unchanged. Fedify also does not copy or reconcile actors, objects, or collections between gateways, so running an actor on several gateways means keeping their data in sync yourself. There is no workflow for rotating a DID's key or migrating an actor to another DID. Fedify resolves verification methods only for did:key DIDs with Ed25519 keys; other DID methods verify only if your document loader resolves them. Gateways must be plain origins, since the arbitrary gateway paths that FEP-ef61 discusses are not supported. The full list is in Not supported.
The profile was tested against tootik v0.25.4 and Mitra v5.10.0 in #1202. Fedify verifies tootik's portable actors, activities, and HTTP Signatures, and Mitra's gateway HTTP Signatures on activities that a client signed. Both accept the portable activities that Fedify sends. Two fixes came out of this testing. Fedify now reads a gateways list that tootik leaves unmapped in its JSON-LD context. It also serializes gateways as bare origins, because Mitra rejects a trailing slash. Fedify has not yet verified an actor or activity that Mitra signed itself, and Mastodon's handling of portable actors has not been tested. The FEP-ef61 interoperability section of FEDERATION.md records what was tested and how.
fedify lookup looks up portable objects by their ap: and ap+ef61: IDs, compatible identifiers, and the handles of portable actors, and it verifies their proofs. When no gateway returns an acceptable object, it says why. A new --gateway option on fedify lookup, fedify inbox, and fedify webfinger names the gateways to ask. fedify inbox --follow follows portable actors, and fedify webfinger accepts portable actor IDs. See Looking up portable objects in the CLI manual.
The mock federation and contexts in @fedify/testing support portable IDs, verification against fixture document loaders, gateway requests, and hashlink media, so tests can exercise portable objects without a live gateway. @fedify/lint's actor URI rules accept the portable IDs that the getPortable*Uri() methods build. @fedify/next now passes hashlink media requests to Fedify, a problem reported in #1149 and fixed in #1170; to have Next.js run the middleware for them, add { source: "/.well-known/apgateway/:path*" } to your matcher.
Fedify already runs activity delivery and inbox processing on queue workers, but applications usually have jobs of their own: digest emails, timeline rebuilds, link previews. Until now, those jobs could not use Fedify's queues, serialization, or retries. Fedify 2.4.0 opens the same machinery to application-defined tasks.
A task is defined once with defineTask() and enqueued from any Context with enqueueTask() or enqueueTaskMany(). Every task takes a Standard Schema, such as a Zod or Valibot schema, which types its payload. Fedify validates the payload when it is enqueued and again when a worker picks it up; the second check catches payloads from an earlier deployment that no longer match the schema:
import { createExponentialBackoffPolicy } from "@fedify/fedify";
import { z } from "zod";
const sendDigest = federation.defineTask("sendDigest", {
schema: z.object({ userId: z.string(), since: z.date() }),
handler: async (ctx, data) => {
// data is typed as { userId: string; since: Date }
await sendEmail(data.userId, await buildDigest(data.userId, data.since));
},
retryPolicy: createExponentialBackoffPolicy({ maxAttempts: 3 }),
});
// Anywhere with a Context:
await ctx.enqueueTask(sendDigest, { userId: "alice", since: new Date() }, {
delay: { minutes: 30 },
deduplicationKey: "digest:alice",
});
Fedify serializes payloads with devalue, so Date, Map, Set, URL, bigint, Temporal values, circular references, and Activity Vocabulary objects all survive the trip through any message queue backend. A Note in a payload comes back as a Note. Failed handlers are retried with exponential backoff by default, and queues with native retries handle retries themselves. Tasks fall back to the outbox queue unless you give them their own. Use the new task slot of the queue option or a per-task queue for that, and startQueue(ctxData, { queue: "task" }) runs a task-only worker. Setting taskQueueResolution: "strict" turns the fallback into an error.
A deduplicationKey deduplicates enqueues. The feature came from @julian's point in #206 that recurring tasks are hard to manage when you cannot tell whether one is already queued. A queue that declares nativeDeduplication enforces the key itself; otherwise, Fedify keeps a best-effort marker in the key–value store through cas(). Each task runs in a fedify.task span, and the queue metrics from 2.3.0 now include task queues. The Background tasks chapter covers routing, retries, deduplication, and the limitations; cron-style scheduling, result backends, and priorities are out of scope for now.
ChanHaeng Lee (@2chanhaeng) implemented the API: the core in #803, deduplication in #806, and observability in #812, following the plans in #797, #798, and #799. The work was merged in #923, closing #206, which had been open since February 2025.
Fedify 2.2.0 added client-to-server outbox listeners, but a C2S client also has to upload images and video before it can post them. That traffic does not go through POST /outbox. Fedify 2.4.0 supports the ActivityPub Media Upload extension, a SocialCG proposal last updated in 2017 that never reached W3C Recommendation status.
setMediaUploader() registers a multipart/form-data endpoint. Its callback receives the uploaded file and the object shell the client posted along with it. If the callback returns the created object, Fedify responds with 201 Created; if it returns a URL where the object will appear once processing finishes, Fedify responds with 202 Accepted:
federation
.setMediaUploader(
"/users/{identifier}/media",
async (ctx, identifier, file, object) => {
const stored = await uploadToStorage(file);
return new Image({
id: ctx.getObjectUri(Image, { uuid: stored.uuid }),
url: new URL(stored.publicUrl),
mediaType: file.type,
name: object.name,
});
},
)
.authorize(async (ctx, identifier) => {
const session = await verifyAccessToken(
ctx.request.headers.get("authorization"),
);
return session?.identifier === identifier;
});
The actor dispatcher advertises the endpoint itself by setting endpoints: new Endpoints({ uploadMedia: ctx.getMediaUploaderUri(identifier) }), the same way it places its inbox and outbox URIs. Without an authorize() hook, anyone who finds the endpoint can upload to it. For that reason, Fedify logs a warning when an uploader is registered without one, and when the returned object's URI does not match an object dispatcher or the actor does not advertise the endpoint. Four new @fedify/lint rules catch the same mistakes before they run. Fedify reads the upload into memory and sets no size limit of its own, so cap the request size at your reverse proxy. Storing the file and serving the object back are up to the application. See the Media upload chapter; the work is in #754 and #927.
Fedify decides several things about every inbox delivery: which signature mechanisms applied, which keys they tried, whether the actor owned the key, and what happened to the activity. Before this release, applications saw only part of that. A Linked Data Signature by a key the actor did not own was counted the same as a bad signature, and the key that verified a request was never handed to the application. The request in #1191 came from an effort to log these decisions for a debugging tool.
The new onRequestFinished() callback on inbox listeners receives an InboxRequestReport for every delivery to an inbox route, including rejected requests and requests that fail before verification:
federation
.setInboxListeners("/users/{identifier}/inbox", "/inbox")
.on(Follow, async (ctx, follow) => { /* ... */ })
.onRequestFinished(async (ctx, report) => {
await db.insertInboxLog({
inbox: report.inbox.kind, // "personal" | "shared" | "portable"
authentication: report.authentication.status,
outcome: report.outcome.type === "response"
? report.outcome.disposition // "processed" | "enqueued" | "duplicate" | ...
: `exception:${report.outcome.stage}`,
keyIds: report.attempts
.flatMap((attempt) => attempt.checks)
.flatMap((check) => check.triedKeys)
.map((key) => key.id?.href),
});
});
Each HTTP Signature, Linked Data Signature, or Object Integrity Proof check appears as an attempt with the actual keys it tried, including a stale cached key and its refreshed replacement. The report also carries the final authentication decision, such as actorKeyMismatch or proofPolicy, and the response or exception that ended the request. Fedify awaits the callback whether or not the trace is sampled. Errors it throws are logged without changing the response, and enqueued means the queue accepted the activity, not that a worker later processed it. Mock federations in @fedify/testing accept the same callback. See Observing inbox requests; the implementation is in #1201.
Actor dispatchers have been able to return a Tombstone since 2.2.0, but object dispatchers could only return null. A deleted post was answered with 404 Not Found, as if it had never existed, so applications that wanted 410 Gone, as ActivityPub recommends, needed their own route in front of Fedify.
Object dispatchers may now return a Tombstone, which Fedify serves with 410 Gone and the serialized tombstone after applying the authorization predicate:
federation.setObjectDispatcher(
Note,
"/users/{identifier}/notes/{id}",
async (ctx, values) => {
const post = await getPost(values.identifier, values.id);
if (post == null) return null;
if (post.deletedAt != null) {
return new Tombstone({
id: ctx.getObjectUri(Note, values),
formerType: Note,
deleted: post.deletedAt,
});
}
return new Note({ id: ctx.getObjectUri(Note, values), content: post.content });
},
);
The return type of ObjectDispatcher widens to TObject | Tombstone | null. RequestContext.getObject() still returns null for a tombstone unless you pass { tombstone: "passthrough" } or ask for the Tombstone or Object class itself. Dispatchers registered for Tombstone or Object that already returned tombstones now respond with 410 rather than 200. See Deleted objects; the change was proposed in #1112 and made in #1117.
Much of what Fedify fetches is chosen by the sender of an incoming request. Key IDs in signatures are one example, and the servers behind them can be slow on purpose. Several changes in 2.4.0 bound that work.
As #1130 points out, a request may carry several RFC 9421 signatures, and each one can make Fedify fetch a key from a URL of the sender's choosing. Fedify now verifies at most the first three, in Signature-Input order, and ignores the rest. A signature counts toward the limit even if it fails before its key is fetched, and a key named by several signatures is looked up once. The new FederationOptions.maxHttpSignatures option, added with the limit in #1166, changes it.
The built-in document loaders now give each call ten seconds by default, a limit proposed in #1131 and added in #1169. The time limit covers every redirect, alternate document, double-knocking retry, and the response body. A timed-out call throws a FetchError whose cause is a DOMException named "TimeoutError". Error responses are read in full before the loader throws, up to 1 MiB, so that a server cannot send a 404 header and then stall the body. FederationOptions.documentLoaderTimeout changes the limit or turns it off with null, and custom document loaders are not affected. Since #913, getDocumentLoader() also rejects HTML responses that do not advertise an ActivityPub alternate with a FetchError, rather than failing inside JSON.parse(), which #912 found cluttering error trackers.
Cached public keys and remembered HTTP Message Signatures specs used to live forever in the key–value store. #1017 reports a production Redis instance holding 194,424 public-key entries, about 165 MiB, for actors that had mostly stopped federating. Keys now expire after 30 days and specs after 90, configurable through FederationOptions.publicKeyTtl and FederationOptions.httpMessageSignaturesSpecTtl. Entries written by earlier versions have no expiry until they are next written. The Clearing legacy cache entries section shows how to remove them for Redis, PostgreSQL, and other stores. Heewon Chae (@heeoneie) implemented this in #1027, their first contribution to Fedify.
const federation = createFederation<void>({
kv,
maxHttpSignatures: 1, // default: 3
documentLoaderTimeout: { seconds: 5 }, // default: 10 seconds
publicKeyTtl: { days: 7 }, // default: 30 days
httpMessageSignaturesSpecTtl: { days: 30 }, // default: 90 days
});
Finally, verifyProof() now authenticates every option of an Ed25519 JCS proof except proofValue, including expires, domain, challenge, and nonce. It also rejects expired or malformed ones. Previously, a field such as expires could be added to an already signed proof without invalidating it. Jiwon Kwon (@z9mb1) spotted this in review of #968. Callers can now require a domain and challenge through VerifyProofOptions to prevent cross-domain or replayed use.
@fedify/adonisjs@fedify/adonisjs integrates Fedify with AdonisJS. Running node ace add @fedify/adonisjs registers a service provider that owns the federation's lifecycle and a server middleware that mounts it. It also scaffolds config/fedify.ts, start/federation.ts, and app/federation/main.ts. Controllers get a ctx.federation request context:
// app/federation/main.ts
import federation from "@fedify/adonisjs/services/builder";
import { Person } from "@fedify/vocab";
federation.setActorDispatcher("/actors/{identifier}", (ctx, identifier) =>
new Person({ id: ctx.getActorUri(identifier), preferredUsername: identifier }));
The middleware sits first in AdonisJS's server stack rather than its router stack. That way Fedify sees paths the router does not know, such as /.well-known/webfinger, and reads request bodies before the body parser consumes them. The package targets Node.js and is published to npm only. See the AdonisJS section of the integration guide.
Samuel Brinkmann (@sabrinkmann) contributed the package in #1006, closing #139, and built it on a prototype that Emelia Smith (@thisismissem) attached to that issue.
npm add @fedify/adonisjs
pnpm add @fedify/adonisjs
yarn add @fedify/adonisjs
@fedify/interaction-controlsGoToSocial interaction controls, FEP-044f quote authorization, and FEP-7aa9 featured collections share a request-and-authorization flow. A server asks to reply to, quote, or feature a post, and the author's server approves according to the post's interactionPolicy. @fedify/interaction-controls implements that flow for like, reply, announce, quote, and feature interactions. Each has a helper, such as likeInteraction or quoteInteraction, that creates and verifies requests and authorizations, evaluates policies, and recognizes interactions that arrived without a request:
import { likeInteraction } from "@fedify/interaction-controls";
// When a LikeRequest arrives:
const verified = await likeInteraction.verifyRequest(ctx, { request });
if (verified.verified) {
const decision = await likeInteraction.evaluatePolicy(ctx, {
subject: note,
requester: request.actorId!,
});
if (decision.result === "automatic") { // or "manual" or "denied"
const authorization = likeInteraction.createAuthorization({
id: new URL("https://example.com/authorizations/1"),
attributedTo: owner,
interactingObject: like,
interactionTarget: note,
});
// ...
}
}
The package does not install inbox listeners or store anything; the application decides where requests and authorizations live and when a human needs to approve. Verification fails closed. Without a policy, likes, replies, and announces are approved automatically, while quotes and features are denied. @fedify/vocab gains the FEP-7aa9 types this needs: FeaturedCollection, FeaturedItem, FeatureRequest, and FeatureAuthorization, along with actors' featuredCollections and InteractionPolicy.canFeature. Samuel Brinkmann asked for them in #810 once Mastodon 4.6 shipped the FEP. See the Interaction controls chapter; the work is in #914 and #929.
npm add @fedify/interaction-controls
pnpm add @fedify/interaction-controls
yarn add @fedify/interaction-controls
deno add jsr:@fedify/interaction-controls
bun add @fedify/interaction-controls
@fedify/netlify@fedify/netlify lets a Fedify application run on Netlify Functions without an external database or queue. NetlifyMessageQueue submits jobs as Netlify Async Workloads events, and createNetlifyQueueHandler() turns a Function into their consumer, with delayed delivery, native retries, and per-key FIFO ordering. NetlifyBlobsKvStore is a key–value store on Netlify Blobs with expiration, prefix listing, and compare-and-swap:
import {
createNetlifyQueueHandler,
NetlifyBlobsKvStore,
NetlifyMessageQueue,
} from "@fedify/netlify";
import { AsyncWorkloadsClient } from "@netlify/async-workloads";
import { getStore } from "@netlify/blobs";
export const kv = new NetlifyBlobsKvStore(
getStore({ name: "fedify", consistency: "strong" }),
);
export const queue = new NetlifyMessageQueue({
client: new AsyncWorkloadsClient(),
orderingKv: kv,
});
// netlify/functions/fedify-queue.ts
export default createNetlifyQueueHandler({
queue,
maxRetries: 4,
federation: () => builder.build({ kv, queue, manuallyStartQueue: true }),
});
The key–value store exists because of cost. #1010 describes a static blog whose ActivityPub traffic kept a Netlify Database from ever idling, which used most of the account's monthly credits. Jiwon Kwon contributed NetlifyBlobsKvStore in #1029, and the queue landed in #934. See Netlify Functions in the deployment guide.
npm add @fedify/netlify
pnpm add @fedify/netlify
yarn add @fedify/netlify
deno add jsr:@fedify/netlify
bun add @fedify/netlify
@fedify/pglite@fedify/pglite provides PgliteKvStore, a KvStore on an embedded PGlite database that you create and pass in. It uses the same schema as PostgresKvStore. It is meant for a single PGlite instance in a single runtime isolate, so it does not include a message queue: PGlite does not share data between processes.
import { PGlite } from "@electric-sql/pglite";
import { PgliteKvStore } from "@fedify/pglite";
const federation = createFederation({
kv: new PgliteKvStore(new PGlite("./data/fedify")),
});
ChanHaeng Lee contributed the package in #1020, together with testKvStore() in @fedify/testing. testKvStore() is a conformance suite for KvStore implementations, the counterpart to testMessageQueue(). See PgliteKvStore.
npm add @fedify/pglite
pnpm add @fedify/pglite
yarn add @fedify/pglite
deno add jsr:@fedify/pglite
bun add @fedify/pglite
Portable inbox forwarding, task deduplication, Netlify queue ordering, and the delivery circuit breaker all need an atomic compare-and-swap on the key–value store. PostgresKvStore now implements cas(), added in #934, and so does RedisKvStore, requested in #1163 and added in #1167. The Redis version uses a single-key Lua script, which works on standalone Redis and Redis Cluster. Before this, the circuit breaker fell back to racy updates on Redis.
Both come with conditions. RedisKvStore needs Redis to permit EVAL, and custom codecs must encode equal values identically. PostgresKvStore now creates logged tables by default, because losing sequence state in crash recovery could block queued events for good. It migrates existing unlogged tables when it initializes. That one-time migration rewrites the table and locks it exclusively, so schedule the upgrade accordingly if the table is large or busy; unlogged: true keeps the old behavior.
fedify init and the CLIfedify init can now scaffold a SvelteKit project, contributed by Jang Hanarae (@menele) in #971. Jang Hanarae also added a test task to generated projects in #990. It starts the app, waits for it to be ready, and checks that it resolves a local actor, which gives new projects a smoke test to run after scaffolding and after later changes. Before generating anything, fedify init now checks that the selected Deno, Bun, or Node.js meets Fedify's minimum version, or a framework's higher one, such as Astro's Node.js 22.12. It fails with a clear error in non-interactive mode, and in interactive mode it disables the affected package managers. Lee Jeongmin (@userjmmm) contributed the check in #981. Astro projects now use Astro 7, and since #936 @fedify/astro is tested against Astro 5, 6, and 7 with real builds on Node.js, Deno, and Bun, as #931 asked.
fedify tunnel gains fedify.com.es, a tunneling service run by the Fedify project that needs no account; the CLI pins its SSH host key and refuses a mismatched server before exposing a local port. localhost.run is removed in the same change, #940, because the service no longer exists. fedify lookup now times out each request after ten seconds when -T/--timeout is not given, and the option now also bounds the wait for a DNS lookup. The CLI moves to Optique 1.3.2 in #1197, which handles attached values like --timeout=30 consistently, gives better suggestions for mistyped options, and no longer hides errors for known options behind positional arguments before --.
@fedify/lint gains several rules beyond the media upload ones mentioned above. actor-preferred-username-required, requested in #895 and added in #1022, warns when an actor dispatcher returns an actor without preferredUsername. outbox-listener-delivery-not-awaited, from #1057 and #1067, reports an outbox listener that calls ctx.sendActivity() or ctx.forwardActivity() and drops the promise. On Cloudflare Workers, such an activity may never leave, because pending work is discarded once the response returns. await, return, Promise.all(), Promise.allSettled(), and waitUntil() count as handling the promise, while void, Promise.race(), and Promise.any() are accepted as a deliberate choice not to wait. The rule is a warning in the ESLint recommended configuration and an error in strict. Oxlint users enable it by name, and it is not available in Deno Lint.
In #1050, which resolves #900, the existing outbox-listener-delivery-required rule began following control flow instead of scanning the listener's source as text. It reports a listener whose only delivery calls sit in a dead branch, after an unconditional return or throw, or in a function that is never used, and it stays quiet when it cannot tell. Jae-Hyuk-Jang (@jhyuk) contributed this change and both new rules. archie (@ArchieTansaria) then taught both delivery rules about statically false loops and loop binding patterns in #1088. See the linting guide.
@fedify/vocab supports the FEP-22cd draft for translations. A Translation records a translated version's language, translators, source object, and source review time, and the new Object.translations property attaches them to a post without creating separate posts. See Translation metadata. (#1037, #1038)@fedify/vocab-tools gains an extraContext property option, which adds a JSON-LD context only when its terms are used, and a trustEmbeddedObjects type option. (#1037, #1038)suppressError: true handles are now logged as warnings rather than errors, so that they no longer show up as application errors. Contributed by Jae Hui Hong (@axz0612). (#933, #1035)enqueueMany() makes separate sends can set MessageQueue.atomicEnqueueMany to false. Fedify then rejects a multi-message batch governed by one deduplicationKey up front, since a partial send followed by a retry could enqueue duplicates. (#930, #934)Temporal bundle temporal-polyfill instead of requiring @js-temporal/polyfill at runtime, and type declarations rely on the standard esnext.temporal lib. (#823, #925)Accept header now includes the ActivityStreams profile on the JSON-LD media type, as ActivityPub and FEP-ef61 gateways require. (#834, #1077)fedify lookup --recurse reporting a timeout or other network failure as a possibly private object; it now reports the actual cause. (#1156, #1199)-p/--allow-private-address option of fedify webfinger being ignored. (#1156, #1199)outbox-listener-delivery-required and outbox-listener-delivery-not-awaited losing track of an object's functions when a nested helper assigns another property of that object. (#1125, #1140)npm add @fedify/fedify@2.4.0
pnpm add @fedify/fedify@2.4.0
yarn add @fedify/fedify@2.4.0
deno add jsr:@fedify/fedify@2.4.0
bun add @fedify/fedify@2.4.0
Update the other @fedify/* packages you use to 2.4.0 at the same time. To update the CLI, run npm install -g @fedify/cli@2.4.0, or see the installation instructions for other package managers.
Most applications need no code changes, but these changes in behavior may affect you:
401 Unauthorized unless they carry a valid Object Integrity Proof by the actor's DID. Keys at compatible identifiers, and keys at ordinary URLs that claim a portable owner, are no longer trusted by origin.Context as their options, such as await create.getObject(ctx), now verify the portable objects they dereference, including references to compatible identifiers from objects with ordinary HTTP(S) IDs./.well-known/apgateway/, and Context.sendActivity() may reject an activity that embeds others' portable objects. Next.js applications need the matcher shown above to serve hashlink media.maxHttpSignatures if you need more.fedify lookup does too.Tombstone or Object that return tombstones now respond with 410 Gone instead of 200 OK.PostgresKvStore migrates existing unlogged tables to logged tables on initialization, which locks the table.RedisKvStore needs EVAL permission for cas().Context and RequestContext interfaces need the new getPortable*Uri() methods and isSignedByAudience().fedify tunnel no longer supports localhost.run.Thanks to this release's contributors:
@fedify/adonisjs, and the request for FEP-7aa9 vocabularytest task in fedify initfedify initactor-preferred-username-required and outbox-listener-delivery-not-awaited lint rules, and control-flow analysis for outbox-listener-delivery-requiredChanHaeng Lee (@2chanhaeng) and Jiwon Kwon (@z9mb1) joined as maintainers after 2.3.0. ChanHaeng built the background task API and @fedify/pglite, Jiwon built NetlifyBlobsKvStore and rewrote the relay documentation, and both reviewed much of the rest of this release.
Many people added tests, documentation, and infrastructure during this cycle, often starting from a good first issue: Seoa Yoon (@bananamilk452), Lim Kyoujin (@dktsudgg), @m2nsp, Cho Seongmin (@seongmin36), Safal Shrestha (@Safal-Shrestha-SS), Jo Youngjae (@jaonz6057), @kecan0406, Loi Nguyen (@lntutor), Oh Taejun (@0w0), Yerin Park (@dstforye), Lee Dogeon (@moreal), and Sae Jin Kim (@siliconsjang).
The FEP-ef61 work owes a lot to people outside the project. @anotherdoesnm opened the request in #288. @silverpill, the author of FEP-ef61 and FEP-ae97, clarified the gateway trust model, reviewed the compound proof profile, and checked its test vectors. @dmitrizagidulin and @by_caballero weighed in on the profile's JSON-LD questions in #938. Testing against tootik and Mitra led to two of the interoperability fixes in this release.
See CHANGES.md for the complete changelog.
github.com
ActivityPub server framework in TypeScript. Contribute to fedify-dev/fedify development by creating an account on GitHub.
Fedify 2.4.0 is out.
The main addition is support for FEP-ef61 portable objects. A Fedify server can now host actors whose identity is a DID instead of a domain, serve their signed objects through /.well-known/apgateway/, and verify the portable objects of other servers. I tested it against tootik and Mitra, and wrote up where Fedify's profile differs from the FEP and what it leaves out.
The release also adds application-defined background tasks, the ActivityPub Media Upload extension, per-request inbox reports, 410 Gone for deleted objects, and four new packages (AdonisJS, Netlify, PGlite, and interaction controls).
Some defaults changed, so please read the upgrading section before you update, especially if you federate with software that uses FEP-ef61 compatible identifiers.
Release notes: https://hackers.pub/@fedify/2026/fedify-2-4.
Thanks to everyone who contributed to this cycle, and to @silverpill for patient answers about the FEP.
hackers.pub
Fedify is a TypeScript framework for building ActivityPub servers. It implements federation details such as HTTP Signatures, JSON-LD processing, WebFinger, inbox and outbox routing, and activity delivery.
Fedify 2.4.0 is a minor release with an unusually long changelog. The main addition is support for FEP-ef61 portable objects: a Fedify server can now host actors whose identity is a DID rather than a domain, serve and verify their signed objects, and consume the portable objects of other servers. The release also adds an API for application-defined background tasks, the ActivityPub Media Upload extension, a hook that reports how every inbox request was authenticated and handled, and 410 Gone tombstones for deleted objects. Several defaults change to limit what a hostile peer can make Fedify do. Four new packages ship with this version: @fedify/adonisjs, @fedify/interaction-controls, @fedify/netlify, and @fedify/pglite.
Most applications can upgrade without code changes, but a few of the new defaults are behavior changes. They are listed in the Upgrading section at the end.
An ActivityPub object's ID is an HTTP(S) URL on its server's domain. If that server shuts down, its IDs stop resolving, and an actor that moves elsewhere gets a new ID that everyone has to follow again. FEP-ef61, written by @silverpill of Mitra, proposes IDs that do not name a server at all. A portable ID is an ap: URI whose authority is a DID:
ap://did:key:z6MkrJVnaZkeFzdQyMZu1cgjg7k1pZZ6pvBQ7XJPt4swbTQ2/actor
The same object can be stored on several servers, called gateways, each of which serves it under /.well-known/apgateway/. Because no server owns the ID, a portable actor, activity, or object is authenticated only by its FEP-8b32 Object Integrity Proof, made with a key of the DID in its ID. A portable actor lists its gateways in a gateways property, and other servers deliver to its inbox through them.
Fedify 2.4.0 implements a profile of FEP-ef61 for gateways and consumers of portable objects. It makes two deliberate choices where the FEP or the specifications it rests on leave questions open, and it leaves out a few parts of the proposal; both are described below. The new Portable objects chapter of the manual walks through running portable actors from start to finish and then describes the profile as a whole, so that other implementers can tell what to expect from a Fedify server. The request came from @anotherdoesnm in #288, which tracked the work from a roadmap to the final pieces.
Portable objects are a foundation for nomadic identity, not nomadic identity itself. Fedify serves and verifies portable objects and forwards activities between gateways, but it does not copy actors or posts between servers, rotate keys, or move an actor to another DID. Full nomadic identity is still open in #413.
A portable actor starts with an Ed25519 key pair. Its DID is made from the public key with exportDidKey(), and whoever holds the private key can act as the actor, so store it with the actor and keep it safe:
import { generateCryptoKeyPair } from "@fedify/fedify";
import { exportDidKey } from "@fedify/vocab-runtime";
const { publicKey, privateKey } = await generateCryptoKeyPair("Ed25519");
const did = await exportDidKey(publicKey); // did:key:z6Mk...
// The verification method that the actor's proofs refer to:
const didKeyId = new URL(`${did}#${did.slice("did:key:".length)}`);
The actor dispatcher returns a portable actor as it would an ordinary one. Context.getPortableActorUri() and its siblings for inboxes, outboxes, collections, and objects build portable IDs from the dispatcher's paths and the DID, and signObject() signs the actor with the DID's key:
federation
.setActorDispatcher("/users/{identifier}", async (ctx, identifier) => {
const user = await findUser(identifier);
if (user == null) return null;
// A request through the gateway endpoint names the DID in its path,
// e.g., GET /.well-known/apgateway/did:key:z6Mk.../users/alice:
if (
ctx.portableRequest != null &&
ctx.portableRequest.authority !== user.did
) {
return null;
}
// This server's gateway keys for the actor (see below):
const keys = await ctx.getActorKeyPairs(identifier);
return await signObject(
new Person({
id: ctx.getPortableActorUri(identifier, user.did), // ap+ef61://did:key:z6Mk.../users/alice
preferredUsername: user.username,
inbox: ctx.getPortableInboxUri(identifier, user.did),
outbox: ctx.getPortableOutboxUri(identifier, user.did),
followers: ctx.getPortableFollowersUri(identifier, user.did),
gateways: [new URL("https://example.com")],
publicKeys: keys.map((key) => key.cryptographicKey),
assertionMethods: keys.map((key) => key.multikey),
}),
user.didKeyPair.privateKey,
new URL(`${user.did}#${user.did.slice("did:key:".length)}`),
);
})
.setKeyPairsDispatcher(async (ctx, identifier) => {
const user = await findUser(identifier);
// This server's own key pair for the actor, not the DID's:
return user == null ? [] : [user.gatewayKeyPair];
})
.mapPortableActorId(async (ctx, identifier) => {
const user = await findUser(identifier);
return user == null ? null : ctx.getPortableActorUri(identifier, user.did);
});
With this dispatcher, Fedify serves the actor at its compatible identifier, https://example.com/.well-known/apgateway/did:key:z6Mk.../users/alice, which is how other servers retrieve it. It serves it through WebFinger as @alice@example.com, since the handle's domain comes from the first gateway in gateways, and at its ordinary URL as before. On the gateway endpoint, Fedify serves a document only if its ID is the requested portable ID and it carries a proof made with a key of that DID.
The key pairs dispatcher and the new ActorCallbackSetters.mapPortableActorId() give the actor gateway keys. These are the server's own keys for the actor, and they sign only the HTTP requests the server makes on the actor's behalf, such as deliveries. They never sign the actor's activities or objects; only the DID's key does that. The actor document lists them under assertionMethods, as FEP-521a describes, so that other servers can verify the HTTP Signatures. See Dispatching the actor and Gateway keys of portable actors for the details.
Object and collection dispatchers serve portable objects and collections through the gateway endpoint in the same way, routed by the path after the DID. Two rules protect the actor's data. A non-public portable object is served only if the dispatcher has an authorization predicate, because FEP-ef61 forbids a gateway to show it to anyone outside its audience; the new RequestContext.isSignedByAudience() helps to write one. Without a predicate, a request for such an object gets 404 Not Found, as though the gateway did not store it. A deleted object is served as a signed Tombstone with 410 Gone. Resources that portable objects refer to by SHA-256 hashlinks, such as GET /.well-known/apgateway/hl:zQm..., are served by the new Federatable.setHashlinkMediaDispatcher().
A portable actor's activities have to be signed by its DID, which Fedify's key pairs dispatcher does not know about. Sign them with signObject() before sending, and pass normalizeExistingProofs: true so that the activity goes out in the form its proof covers. An activity that embeds a portable object, such as the Note of a Create, is signed inside out:
const actor = withGatewayHints(
ctx.getPortableActorUri(user.identifier, user.did),
[new URL("https://example.com")],
); // ap+ef61://did:key:z6Mk.../users/alice?@gateway=https%3A%2F%2Fexample.com
const note = await signObject(
new Note({
id: ctx.getPortableObjectUri(
Note,
{ identifier: user.identifier, id: crypto.randomUUID() },
user.did,
),
attribution: actor,
to: PUBLIC_COLLECTION,
content: "Hello, world!",
}),
user.didKeyPair.privateKey,
didKeyId,
);
const create = await signObject(
new Create({
id: ctx.getPortableObjectUri(
Note,
{ identifier: user.identifier, id: crypto.randomUUID() },
user.did,
),
actor,
to: PUBLIC_COLLECTION,
object: note,
}),
user.didKeyPair.privateKey,
didKeyId,
);
await ctx.sendActivity(
{ identifier: user.identifier },
"followers",
create,
{ normalizeExistingProofs: true },
);
This example depends on a fix to signObject() that is useful outside FEP-ef61 as well. Previously, a signed object assigned to a typed parent was rebuilt under the parent's JSON-LD context when the parent was serialized. The child kept its proofValue but lost the document its proof covered, so it no longer verified on its own. signObject() now captures the exact document it signed, and nested serialization embeds that document verbatim. The captured document is a snapshot: clone() does not carry it, so sign a clone again if it has to be embedded. The design is in #1044 and the fix in #1051.
Context.sendActivity() refuses a portable activity that is unsigned, signed by the wrong key, or carries a Linked Data Signature, and throws a TypeError before anything is queued. It also refuses a few shapes of compound document that Fedify's own inboxes would reject, such as a received signed Follow embedded in an Accept; refer to such an object by its ID instead. Deliveries to portable inboxes go through the recipient's gateways, or else the @gateway hints on the inbox, trying at most five of them until one accepts. Before this release, they failed with UrlError: Unsupported protocol: ap+ef61:. The rules are in Delivering to portable actors and Producing a compound document.
On the receiving side, a portable inbox is reached as POST /.well-known/apgateway/did:key:z6Mk.../users/alice/inbox and dispatched to the same inbox listeners as the ordinary inbox. Context.parseUri() gains a portable option, and the parsed result has the DID in a new authority property to check against the one you store. Fedify accepts an activity of a portable actor, or one with a portable ID, only if it has a valid proof by that DID; neither an HTTP Signature nor a Linked Data Signature authenticates it. Each portable object embedded in the activity is verified against its own proof before dispatch. An accepted activity that carries its own proof is forwarded to the actor's other gateways at most once, as FEP-ef61 recommends. Forwarding needs a key–value store that supports cas(), and the FederationOptions.portableInboxForwarding option tunes it. See Portable inboxes and Forwarding to other gateways.
Applications that never host a portable actor still meet portable objects from other servers. Context.lookupObject() resolves portable IDs, compatible identifiers, and the fediverse handles of portable actors, and returns portable objects only if they verify:
const actor = await ctx.lookupObject("@alice@example.com");
// A portable ID without @gateway hints needs gateways to ask:
const note = await ctx.lookupObject("ap://did:key:z6Mk.../notes/1", {
gateways: ["https://example.com"],
});
Property accessors fetch portable references through gateways as well, and they verify what they fetch with the new verifyPortableObject option. Context has a matching verifyPortableObject property, so passing a context as the options, as in await create.getObject(ctx), is enough. Activities received in inboxes use this verifier by default, and objects fetched through their accessors inherit it, so a chained call such as (await create.getObject(ctx))?.getAttribution() verifies each portable object along the way. A lookup asks at most five gateways, falling back to the next when one fails or serves something that does not verify. Unsigned collections are accepted only from a gateway that the owner lists, and only for an actor's inbox, outbox, followers, following, and liked, as FEP-ef61 allows. fetchPortableMedia() retrieves the media of a portable object and returns it only if it matches the object's digestMultibase. See Dereferencing portable references and Default verifiers.
Software that does not understand ap: URIs can refer to a portable object by a compatible identifier, an HTTP(S) URL under a gateway's /.well-known/apgateway/ path such as https://gw.example/.well-known/apgateway/did:key:z6Mk.../actor. Some implementations, tootik among them, use compatible identifiers as the IDs of their portable actors.
Earlier versions of Fedify treated these URLs as ordinary web URLs and trusted them by the server that served them. Since anyone can serve a compatible identifier for any DID, https://evil.example/.well-known/apgateway/did:key:z6MkAlice/actor could act as Alice without a proof by Alice's DID. Fedify 2.4.0 treats a compatible identifier as the portable object it stands for, as FEP-ef61 requires. Inboxes reject an activity from an actor whose ID is a compatible identifier with 401 Unauthorized unless it has a valid Object Integrity Proof by the actor's DID, and there is no option to turn this off. Actors that publish compatible identifiers without signing them are no longer accepted, and neither are those whose DIDs use key types that Fedify cannot verify yet. The change was tracked in #1093 and made in #1105.
Your own portable actors may still use compatible identifiers as their IDs, for software that rejects ap: IDs. Build them with toCompatibleEf61Id() before signing, and Fedify will treat them as portable wherever it serves or sends them. It also warns if one is malformed or does not point at the actor's first gateway. See Compatible identifiers as actor IDs.
Two choices in Fedify's profile differ from the current FEP text or rest on semantics that are not settled yet. Fedify keeps both for the 2.x series, and either may change in Fedify 3.0.
The first is the scheme. FEP-ef61 recommends ap: and canonicalizes ap+ef61: to it, while also warning that the recommendation may change to ap+ef61:, since these IDs are meant only for portable objects. Fedify accepts both, compares them as equal, and serializes ap+ef61:. Parsing a document normalizes its portable IRIs to ap+ef61:, so re-serializing an object that another implementation signed with ap: IDs breaks its proof. Keep the received JSON of such documents, and compare portable IDs with arePortableUrisEqual() rather than as strings. The discussion is in #826 and #828, and the reasoning is in Canonical ap+ef61: scheme.
The second concerns compound documents. When a signed Note sits inside a signed Create, the verifier has to know which part of the JSON each proof covers, and neither FEP-8b32 nor Verifiable Credential Data Integrity defines that yet; the question is open in w3c/vc-data-integrity#350. Fedify uses an interim map-local profile, worked out in #938. Each JSON map with a proof is its own secured document, each embedded portable object needs its own proof and its own @context, and proof sets and chains are not supported. The profile authenticates the exact JSON values each proof covers; it does not promise that a child's JSON-LD expansion inside its parent matches its expansion on its own. @silverpill, @dmitrizagidulin, and @by_caballero reviewed the proposal in that thread, and @silverpill reproduced its test vectors independently. See Map-local compound proofs.
Fedify does not implement FEP-ae97, the companion proposal for client-side signing. There are no endpoints for registering actors, submitting client-signed activities to a portable outbox, or gateway discovery, and no media upload and deletion endpoints under /.well-known/apgateway-media. Your own outbox listeners can still relay activities that a client signed, unchanged. Fedify also does not copy or reconcile actors, objects, or collections between gateways, so running an actor on several gateways means keeping their data in sync yourself. There is no workflow for rotating a DID's key or migrating an actor to another DID. Fedify resolves verification methods only for did:key DIDs with Ed25519 keys; other DID methods verify only if your document loader resolves them. Gateways must be plain origins, since the arbitrary gateway paths that FEP-ef61 discusses are not supported. The full list is in Not supported.
The profile was tested against tootik v0.25.4 and Mitra v5.10.0 in #1202. Fedify verifies tootik's portable actors, activities, and HTTP Signatures, and Mitra's gateway HTTP Signatures on activities that a client signed. Both accept the portable activities that Fedify sends. Two fixes came out of this testing. Fedify now reads a gateways list that tootik leaves unmapped in its JSON-LD context. It also serializes gateways as bare origins, because Mitra rejects a trailing slash. Fedify has not yet verified an actor or activity that Mitra signed itself, and Mastodon's handling of portable actors has not been tested. The FEP-ef61 interoperability section of FEDERATION.md records what was tested and how.
fedify lookup looks up portable objects by their ap: and ap+ef61: IDs, compatible identifiers, and the handles of portable actors, and it verifies their proofs. When no gateway returns an acceptable object, it says why. A new --gateway option on fedify lookup, fedify inbox, and fedify webfinger names the gateways to ask. fedify inbox --follow follows portable actors, and fedify webfinger accepts portable actor IDs. See Looking up portable objects in the CLI manual.
The mock federation and contexts in @fedify/testing support portable IDs, verification against fixture document loaders, gateway requests, and hashlink media, so tests can exercise portable objects without a live gateway. @fedify/lint's actor URI rules accept the portable IDs that the getPortable*Uri() methods build. @fedify/next now passes hashlink media requests to Fedify, a problem reported in #1149 and fixed in #1170; to have Next.js run the middleware for them, add { source: "/.well-known/apgateway/:path*" } to your matcher.
Fedify already runs activity delivery and inbox processing on queue workers, but applications usually have jobs of their own: digest emails, timeline rebuilds, link previews. Until now, those jobs could not use Fedify's queues, serialization, or retries. Fedify 2.4.0 opens the same machinery to application-defined tasks.
A task is defined once with defineTask() and enqueued from any Context with enqueueTask() or enqueueTaskMany(). Every task takes a Standard Schema, such as a Zod or Valibot schema, which types its payload. Fedify validates the payload when it is enqueued and again when a worker picks it up; the second check catches payloads from an earlier deployment that no longer match the schema:
import { createExponentialBackoffPolicy } from "@fedify/fedify";
import { z } from "zod";
const sendDigest = federation.defineTask("sendDigest", {
schema: z.object({ userId: z.string(), since: z.date() }),
handler: async (ctx, data) => {
// data is typed as { userId: string; since: Date }
await sendEmail(data.userId, await buildDigest(data.userId, data.since));
},
retryPolicy: createExponentialBackoffPolicy({ maxAttempts: 3 }),
});
// Anywhere with a Context:
await ctx.enqueueTask(sendDigest, { userId: "alice", since: new Date() }, {
delay: { minutes: 30 },
deduplicationKey: "digest:alice",
});
Fedify serializes payloads with devalue, so Date, Map, Set, URL, bigint, Temporal values, circular references, and Activity Vocabulary objects all survive the trip through any message queue backend. A Note in a payload comes back as a Note. Failed handlers are retried with exponential backoff by default, and queues with native retries handle retries themselves. Tasks fall back to the outbox queue unless you give them their own. Use the new task slot of the queue option or a per-task queue for that, and startQueue(ctxData, { queue: "task" }) runs a task-only worker. Setting taskQueueResolution: "strict" turns the fallback into an error.
A deduplicationKey deduplicates enqueues. The feature came from @julian's point in #206 that recurring tasks are hard to manage when you cannot tell whether one is already queued. A queue that declares nativeDeduplication enforces the key itself; otherwise, Fedify keeps a best-effort marker in the key–value store through cas(). Each task runs in a fedify.task span, and the queue metrics from 2.3.0 now include task queues. The Background tasks chapter covers routing, retries, deduplication, and the limitations; cron-style scheduling, result backends, and priorities are out of scope for now.
ChanHaeng Lee (@2chanhaeng) implemented the API: the core in #803, deduplication in #806, and observability in #812, following the plans in #797, #798, and #799. The work was merged in #923, closing #206, which had been open since February 2025.
Fedify 2.2.0 added client-to-server outbox listeners, but a C2S client also has to upload images and video before it can post them. That traffic does not go through POST /outbox. Fedify 2.4.0 supports the ActivityPub Media Upload extension, a SocialCG proposal last updated in 2017 that never reached W3C Recommendation status.
setMediaUploader() registers a multipart/form-data endpoint. Its callback receives the uploaded file and the object shell the client posted along with it. If the callback returns the created object, Fedify responds with 201 Created; if it returns a URL where the object will appear once processing finishes, Fedify responds with 202 Accepted:
federation
.setMediaUploader(
"/users/{identifier}/media",
async (ctx, identifier, file, object) => {
const stored = await uploadToStorage(file);
return new Image({
id: ctx.getObjectUri(Image, { uuid: stored.uuid }),
url: new URL(stored.publicUrl),
mediaType: file.type,
name: object.name,
});
},
)
.authorize(async (ctx, identifier) => {
const session = await verifyAccessToken(
ctx.request.headers.get("authorization"),
);
return session?.identifier === identifier;
});
The actor dispatcher advertises the endpoint itself by setting endpoints: new Endpoints({ uploadMedia: ctx.getMediaUploaderUri(identifier) }), the same way it places its inbox and outbox URIs. Without an authorize() hook, anyone who finds the endpoint can upload to it. For that reason, Fedify logs a warning when an uploader is registered without one, and when the returned object's URI does not match an object dispatcher or the actor does not advertise the endpoint. Four new @fedify/lint rules catch the same mistakes before they run. Fedify reads the upload into memory and sets no size limit of its own, so cap the request size at your reverse proxy. Storing the file and serving the object back are up to the application. See the Media upload chapter; the work is in #754 and #927.
Fedify decides several things about every inbox delivery: which signature mechanisms applied, which keys they tried, whether the actor owned the key, and what happened to the activity. Before this release, applications saw only part of that. A Linked Data Signature by a key the actor did not own was counted the same as a bad signature, and the key that verified a request was never handed to the application. The request in #1191 came from an effort to log these decisions for a debugging tool.
The new onRequestFinished() callback on inbox listeners receives an InboxRequestReport for every delivery to an inbox route, including rejected requests and requests that fail before verification:
federation
.setInboxListeners("/users/{identifier}/inbox", "/inbox")
.on(Follow, async (ctx, follow) => { /* ... */ })
.onRequestFinished(async (ctx, report) => {
await db.insertInboxLog({
inbox: report.inbox.kind, // "personal" | "shared" | "portable"
authentication: report.authentication.status,
outcome: report.outcome.type === "response"
? report.outcome.disposition // "processed" | "enqueued" | "duplicate" | ...
: `exception:${report.outcome.stage}`,
keyIds: report.attempts
.flatMap((attempt) => attempt.checks)
.flatMap((check) => check.triedKeys)
.map((key) => key.id?.href),
});
});
Each HTTP Signature, Linked Data Signature, or Object Integrity Proof check appears as an attempt with the actual keys it tried, including a stale cached key and its refreshed replacement. The report also carries the final authentication decision, such as actorKeyMismatch or proofPolicy, and the response or exception that ended the request. Fedify awaits the callback whether or not the trace is sampled. Errors it throws are logged without changing the response, and enqueued means the queue accepted the activity, not that a worker later processed it. Mock federations in @fedify/testing accept the same callback. See Observing inbox requests; the implementation is in #1201.
Actor dispatchers have been able to return a Tombstone since 2.2.0, but object dispatchers could only return null. A deleted post was answered with 404 Not Found, as if it had never existed, so applications that wanted 410 Gone, as ActivityPub recommends, needed their own route in front of Fedify.
Object dispatchers may now return a Tombstone, which Fedify serves with 410 Gone and the serialized tombstone after applying the authorization predicate:
federation.setObjectDispatcher(
Note,
"/users/{identifier}/notes/{id}",
async (ctx, values) => {
const post = await getPost(values.identifier, values.id);
if (post == null) return null;
if (post.deletedAt != null) {
return new Tombstone({
id: ctx.getObjectUri(Note, values),
formerType: Note,
deleted: post.deletedAt,
});
}
return new Note({ id: ctx.getObjectUri(Note, values), content: post.content });
},
);
The return type of ObjectDispatcher widens to TObject | Tombstone | null. RequestContext.getObject() still returns null for a tombstone unless you pass { tombstone: "passthrough" } or ask for the Tombstone or Object class itself. Dispatchers registered for Tombstone or Object that already returned tombstones now respond with 410 rather than 200. See Deleted objects; the change was proposed in #1112 and made in #1117.
Much of what Fedify fetches is chosen by the sender of an incoming request. Key IDs in signatures are one example, and the servers behind them can be slow on purpose. Several changes in 2.4.0 bound that work.
As #1130 points out, a request may carry several RFC 9421 signatures, and each one can make Fedify fetch a key from a URL of the sender's choosing. Fedify now verifies at most the first three, in Signature-Input order, and ignores the rest. A signature counts toward the limit even if it fails before its key is fetched, and a key named by several signatures is looked up once. The new FederationOptions.maxHttpSignatures option, added with the limit in #1166, changes it.
The built-in document loaders now give each call ten seconds by default, a limit proposed in #1131 and added in #1169. The time limit covers every redirect, alternate document, double-knocking retry, and the response body. A timed-out call throws a FetchError whose cause is a DOMException named "TimeoutError". Error responses are read in full before the loader throws, up to 1 MiB, so that a server cannot send a 404 header and then stall the body. FederationOptions.documentLoaderTimeout changes the limit or turns it off with null, and custom document loaders are not affected. Since #913, getDocumentLoader() also rejects HTML responses that do not advertise an ActivityPub alternate with a FetchError, rather than failing inside JSON.parse(), which #912 found cluttering error trackers.
Cached public keys and remembered HTTP Message Signatures specs used to live forever in the key–value store. #1017 reports a production Redis instance holding 194,424 public-key entries, about 165 MiB, for actors that had mostly stopped federating. Keys now expire after 30 days and specs after 90, configurable through FederationOptions.publicKeyTtl and FederationOptions.httpMessageSignaturesSpecTtl. Entries written by earlier versions have no expiry until they are next written. The Clearing legacy cache entries section shows how to remove them for Redis, PostgreSQL, and other stores. Heewon Chae (@heeoneie) implemented this in #1027, their first contribution to Fedify.
const federation = createFederation<void>({
kv,
maxHttpSignatures: 1, // default: 3
documentLoaderTimeout: { seconds: 5 }, // default: 10 seconds
publicKeyTtl: { days: 7 }, // default: 30 days
httpMessageSignaturesSpecTtl: { days: 30 }, // default: 90 days
});
Finally, verifyProof() now authenticates every option of an Ed25519 JCS proof except proofValue, including expires, domain, challenge, and nonce. It also rejects expired or malformed ones. Previously, a field such as expires could be added to an already signed proof without invalidating it. Jiwon Kwon (@z9mb1) spotted this in review of #968. Callers can now require a domain and challenge through VerifyProofOptions to prevent cross-domain or replayed use.
@fedify/adonisjs@fedify/adonisjs integrates Fedify with AdonisJS. Running node ace add @fedify/adonisjs registers a service provider that owns the federation's lifecycle and a server middleware that mounts it. It also scaffolds config/fedify.ts, start/federation.ts, and app/federation/main.ts. Controllers get a ctx.federation request context:
// app/federation/main.ts
import federation from "@fedify/adonisjs/services/builder";
import { Person } from "@fedify/vocab";
federation.setActorDispatcher("/actors/{identifier}", (ctx, identifier) =>
new Person({ id: ctx.getActorUri(identifier), preferredUsername: identifier }));
The middleware sits first in AdonisJS's server stack rather than its router stack. That way Fedify sees paths the router does not know, such as /.well-known/webfinger, and reads request bodies before the body parser consumes them. The package targets Node.js and is published to npm only. See the AdonisJS section of the integration guide.
Samuel Brinkmann (@sabrinkmann) contributed the package in #1006, closing #139, and built it on a prototype that Emelia Smith (@thisismissem) attached to that issue.
npm add @fedify/adonisjs
pnpm add @fedify/adonisjs
yarn add @fedify/adonisjs
@fedify/interaction-controlsGoToSocial interaction controls, FEP-044f quote authorization, and FEP-7aa9 featured collections share a request-and-authorization flow. A server asks to reply to, quote, or feature a post, and the author's server approves according to the post's interactionPolicy. @fedify/interaction-controls implements that flow for like, reply, announce, quote, and feature interactions. Each has a helper, such as likeInteraction or quoteInteraction, that creates and verifies requests and authorizations, evaluates policies, and recognizes interactions that arrived without a request:
import { likeInteraction } from "@fedify/interaction-controls";
// When a LikeRequest arrives:
const verified = await likeInteraction.verifyRequest(ctx, { request });
if (verified.verified) {
const decision = await likeInteraction.evaluatePolicy(ctx, {
subject: note,
requester: request.actorId!,
});
if (decision.result === "automatic") { // or "manual" or "denied"
const authorization = likeInteraction.createAuthorization({
id: new URL("https://example.com/authorizations/1"),
attributedTo: owner,
interactingObject: like,
interactionTarget: note,
});
// ...
}
}
The package does not install inbox listeners or store anything; the application decides where requests and authorizations live and when a human needs to approve. Verification fails closed. Without a policy, likes, replies, and announces are approved automatically, while quotes and features are denied. @fedify/vocab gains the FEP-7aa9 types this needs: FeaturedCollection, FeaturedItem, FeatureRequest, and FeatureAuthorization, along with actors' featuredCollections and InteractionPolicy.canFeature. Samuel Brinkmann asked for them in #810 once Mastodon 4.6 shipped the FEP. See the Interaction controls chapter; the work is in #914 and #929.
npm add @fedify/interaction-controls
pnpm add @fedify/interaction-controls
yarn add @fedify/interaction-controls
deno add jsr:@fedify/interaction-controls
bun add @fedify/interaction-controls
@fedify/netlify@fedify/netlify lets a Fedify application run on Netlify Functions without an external database or queue. NetlifyMessageQueue submits jobs as Netlify Async Workloads events, and createNetlifyQueueHandler() turns a Function into their consumer, with delayed delivery, native retries, and per-key FIFO ordering. NetlifyBlobsKvStore is a key–value store on Netlify Blobs with expiration, prefix listing, and compare-and-swap:
import {
createNetlifyQueueHandler,
NetlifyBlobsKvStore,
NetlifyMessageQueue,
} from "@fedify/netlify";
import { AsyncWorkloadsClient } from "@netlify/async-workloads";
import { getStore } from "@netlify/blobs";
export const kv = new NetlifyBlobsKvStore(
getStore({ name: "fedify", consistency: "strong" }),
);
export const queue = new NetlifyMessageQueue({
client: new AsyncWorkloadsClient(),
orderingKv: kv,
});
// netlify/functions/fedify-queue.ts
export default createNetlifyQueueHandler({
queue,
maxRetries: 4,
federation: () => builder.build({ kv, queue, manuallyStartQueue: true }),
});
The key–value store exists because of cost. #1010 describes a static blog whose ActivityPub traffic kept a Netlify Database from ever idling, which used most of the account's monthly credits. Jiwon Kwon contributed NetlifyBlobsKvStore in #1029, and the queue landed in #934. See Netlify Functions in the deployment guide.
npm add @fedify/netlify
pnpm add @fedify/netlify
yarn add @fedify/netlify
deno add jsr:@fedify/netlify
bun add @fedify/netlify
@fedify/pglite@fedify/pglite provides PgliteKvStore, a KvStore on an embedded PGlite database that you create and pass in. It uses the same schema as PostgresKvStore. It is meant for a single PGlite instance in a single runtime isolate, so it does not include a message queue: PGlite does not share data between processes.
import { PGlite } from "@electric-sql/pglite";
import { PgliteKvStore } from "@fedify/pglite";
const federation = createFederation({
kv: new PgliteKvStore(new PGlite("./data/fedify")),
});
ChanHaeng Lee contributed the package in #1020, together with testKvStore() in @fedify/testing. testKvStore() is a conformance suite for KvStore implementations, the counterpart to testMessageQueue(). See PgliteKvStore.
npm add @fedify/pglite
pnpm add @fedify/pglite
yarn add @fedify/pglite
deno add jsr:@fedify/pglite
bun add @fedify/pglite
Portable inbox forwarding, task deduplication, Netlify queue ordering, and the delivery circuit breaker all need an atomic compare-and-swap on the key–value store. PostgresKvStore now implements cas(), added in #934, and so does RedisKvStore, requested in #1163 and added in #1167. The Redis version uses a single-key Lua script, which works on standalone Redis and Redis Cluster. Before this, the circuit breaker fell back to racy updates on Redis.
Both come with conditions. RedisKvStore needs Redis to permit EVAL, and custom codecs must encode equal values identically. PostgresKvStore now creates logged tables by default, because losing sequence state in crash recovery could block queued events for good. It migrates existing unlogged tables when it initializes. That one-time migration rewrites the table and locks it exclusively, so schedule the upgrade accordingly if the table is large or busy; unlogged: true keeps the old behavior.
fedify init and the CLIfedify init can now scaffold a SvelteKit project, contributed by Jang Hanarae (@menele) in #971. Jang Hanarae also added a test task to generated projects in #990. It starts the app, waits for it to be ready, and checks that it resolves a local actor, which gives new projects a smoke test to run after scaffolding and after later changes. Before generating anything, fedify init now checks that the selected Deno, Bun, or Node.js meets Fedify's minimum version, or a framework's higher one, such as Astro's Node.js 22.12. It fails with a clear error in non-interactive mode, and in interactive mode it disables the affected package managers. Lee Jeongmin (@userjmmm) contributed the check in #981. Astro projects now use Astro 7, and since #936 @fedify/astro is tested against Astro 5, 6, and 7 with real builds on Node.js, Deno, and Bun, as #931 asked.
fedify tunnel gains fedify.com.es, a tunneling service run by the Fedify project that needs no account; the CLI pins its SSH host key and refuses a mismatched server before exposing a local port. localhost.run is removed in the same change, #940, because the service no longer exists. fedify lookup now times out each request after ten seconds when -T/--timeout is not given, and the option now also bounds the wait for a DNS lookup. The CLI moves to Optique 1.3.2 in #1197, which handles attached values like --timeout=30 consistently, gives better suggestions for mistyped options, and no longer hides errors for known options behind positional arguments before --.
@fedify/lint gains several rules beyond the media upload ones mentioned above. actor-preferred-username-required, requested in #895 and added in #1022, warns when an actor dispatcher returns an actor without preferredUsername. outbox-listener-delivery-not-awaited, from #1057 and #1067, reports an outbox listener that calls ctx.sendActivity() or ctx.forwardActivity() and drops the promise. On Cloudflare Workers, such an activity may never leave, because pending work is discarded once the response returns. await, return, Promise.all(), Promise.allSettled(), and waitUntil() count as handling the promise, while void, Promise.race(), and Promise.any() are accepted as a deliberate choice not to wait. The rule is a warning in the ESLint recommended configuration and an error in strict. Oxlint users enable it by name, and it is not available in Deno Lint.
In #1050, which resolves #900, the existing outbox-listener-delivery-required rule began following control flow instead of scanning the listener's source as text. It reports a listener whose only delivery calls sit in a dead branch, after an unconditional return or throw, or in a function that is never used, and it stays quiet when it cannot tell. Jae-Hyuk-Jang (@jhyuk) contributed this change and both new rules. archie (@ArchieTansaria) then taught both delivery rules about statically false loops and loop binding patterns in #1088. See the linting guide.
@fedify/vocab supports the FEP-22cd draft for translations. A Translation records a translated version's language, translators, source object, and source review time, and the new Object.translations property attaches them to a post without creating separate posts. See Translation metadata. (#1037, #1038)@fedify/vocab-tools gains an extraContext property option, which adds a JSON-LD context only when its terms are used, and a trustEmbeddedObjects type option. (#1037, #1038)suppressError: true handles are now logged as warnings rather than errors, so that they no longer show up as application errors. Contributed by Jae Hui Hong (@axz0612). (#933, #1035)enqueueMany() makes separate sends can set MessageQueue.atomicEnqueueMany to false. Fedify then rejects a multi-message batch governed by one deduplicationKey up front, since a partial send followed by a retry could enqueue duplicates. (#930, #934)Temporal bundle temporal-polyfill instead of requiring @js-temporal/polyfill at runtime, and type declarations rely on the standard esnext.temporal lib. (#823, #925)Accept header now includes the ActivityStreams profile on the JSON-LD media type, as ActivityPub and FEP-ef61 gateways require. (#834, #1077)fedify lookup --recurse reporting a timeout or other network failure as a possibly private object; it now reports the actual cause. (#1156, #1199)-p/--allow-private-address option of fedify webfinger being ignored. (#1156, #1199)outbox-listener-delivery-required and outbox-listener-delivery-not-awaited losing track of an object's functions when a nested helper assigns another property of that object. (#1125, #1140)npm add @fedify/fedify@2.4.0
pnpm add @fedify/fedify@2.4.0
yarn add @fedify/fedify@2.4.0
deno add jsr:@fedify/fedify@2.4.0
bun add @fedify/fedify@2.4.0
Update the other @fedify/* packages you use to 2.4.0 at the same time. To update the CLI, run npm install -g @fedify/cli@2.4.0, or see the installation instructions for other package managers.
Most applications need no code changes, but these changes in behavior may affect you:
401 Unauthorized unless they carry a valid Object Integrity Proof by the actor's DID. Keys at compatible identifiers, and keys at ordinary URLs that claim a portable owner, are no longer trusted by origin.Context as their options, such as await create.getObject(ctx), now verify the portable objects they dereference, including references to compatible identifiers from objects with ordinary HTTP(S) IDs./.well-known/apgateway/, and Context.sendActivity() may reject an activity that embeds others' portable objects. Next.js applications need the matcher shown above to serve hashlink media.maxHttpSignatures if you need more.fedify lookup does too.Tombstone or Object that return tombstones now respond with 410 Gone instead of 200 OK.PostgresKvStore migrates existing unlogged tables to logged tables on initialization, which locks the table.RedisKvStore needs EVAL permission for cas().Context and RequestContext interfaces need the new getPortable*Uri() methods and isSignedByAudience().fedify tunnel no longer supports localhost.run.Thanks to this release's contributors:
@fedify/adonisjs, and the request for FEP-7aa9 vocabularytest task in fedify initfedify initactor-preferred-username-required and outbox-listener-delivery-not-awaited lint rules, and control-flow analysis for outbox-listener-delivery-requiredChanHaeng Lee (@2chanhaeng) and Jiwon Kwon (@z9mb1) joined as maintainers after 2.3.0. ChanHaeng built the background task API and @fedify/pglite, Jiwon built NetlifyBlobsKvStore and rewrote the relay documentation, and both reviewed much of the rest of this release.
Many people added tests, documentation, and infrastructure during this cycle, often starting from a good first issue: Seoa Yoon (@bananamilk452), Lim Kyoujin (@dktsudgg), @m2nsp, Cho Seongmin (@seongmin36), Safal Shrestha (@Safal-Shrestha-SS), Jo Youngjae (@jaonz6057), @kecan0406, Loi Nguyen (@lntutor), Oh Taejun (@0w0), Yerin Park (@dstforye), Lee Dogeon (@moreal), and Sae Jin Kim (@siliconsjang).
The FEP-ef61 work owes a lot to people outside the project. @anotherdoesnm opened the request in #288. @silverpill, the author of FEP-ef61 and FEP-ae97, clarified the gateway trust model, reviewed the compound proof profile, and checked its test vectors. @dmitrizagidulin and @by_caballero weighed in on the profile's JSON-LD questions in #938. Testing against tootik and Mitra led to two of the interoperability fixes in this release.
See CHANGES.md for the complete changelog.
github.com
ActivityPub server framework in TypeScript. Contribute to fedify-dev/fedify development by creating an account on GitHub.
Fedify 2.4.0 is out.
The main addition is support for FEP-ef61 portable objects. A Fedify server can now host actors whose identity is a DID instead of a domain, serve their signed objects through /.well-known/apgateway/, and verify the portable objects of other servers. I tested it against tootik and Mitra, and wrote up where Fedify's profile differs from the FEP and what it leaves out.
The release also adds application-defined background tasks, the ActivityPub Media Upload extension, per-request inbox reports, 410 Gone for deleted objects, and four new packages (AdonisJS, Netlify, PGlite, and interaction controls).
Some defaults changed, so please read the upgrading section before you update, especially if you federate with software that uses FEP-ef61 compatible identifiers.
Release notes: https://hackers.pub/@fedify/2026/fedify-2-4.
Thanks to everyone who contributed to this cycle, and to @silverpill for patient answers about the FEP.
hackers.pub
Fedify is a TypeScript framework for building ActivityPub servers. It implements federation details such as HTTP Signatures, JSON-LD processing, WebFinger, inbox and outbox routing, and activity delivery.
Fedify 2.4.0 is a minor release with an unusually long changelog. The main addition is support for FEP-ef61 portable objects: a Fedify server can now host actors whose identity is a DID rather than a domain, serve and verify their signed objects, and consume the portable objects of other servers. The release also adds an API for application-defined background tasks, the ActivityPub Media Upload extension, a hook that reports how every inbox request was authenticated and handled, and 410 Gone tombstones for deleted objects. Several defaults change to limit what a hostile peer can make Fedify do. Four new packages ship with this version: @fedify/adonisjs, @fedify/interaction-controls, @fedify/netlify, and @fedify/pglite.
Most applications can upgrade without code changes, but a few of the new defaults are behavior changes. They are listed in the Upgrading section at the end.
An ActivityPub object's ID is an HTTP(S) URL on its server's domain. If that server shuts down, its IDs stop resolving, and an actor that moves elsewhere gets a new ID that everyone has to follow again. FEP-ef61, written by @silverpill of Mitra, proposes IDs that do not name a server at all. A portable ID is an ap: URI whose authority is a DID:
ap://did:key:z6MkrJVnaZkeFzdQyMZu1cgjg7k1pZZ6pvBQ7XJPt4swbTQ2/actor
The same object can be stored on several servers, called gateways, each of which serves it under /.well-known/apgateway/. Because no server owns the ID, a portable actor, activity, or object is authenticated only by its FEP-8b32 Object Integrity Proof, made with a key of the DID in its ID. A portable actor lists its gateways in a gateways property, and other servers deliver to its inbox through them.
Fedify 2.4.0 implements a profile of FEP-ef61 for gateways and consumers of portable objects. It makes two deliberate choices where the FEP or the specifications it rests on leave questions open, and it leaves out a few parts of the proposal; both are described below. The new Portable objects chapter of the manual walks through running portable actors from start to finish and then describes the profile as a whole, so that other implementers can tell what to expect from a Fedify server. The request came from @anotherdoesnm in #288, which tracked the work from a roadmap to the final pieces.
Portable objects are a foundation for nomadic identity, not nomadic identity itself. Fedify serves and verifies portable objects and forwards activities between gateways, but it does not copy actors or posts between servers, rotate keys, or move an actor to another DID. Full nomadic identity is still open in #413.
A portable actor starts with an Ed25519 key pair. Its DID is made from the public key with exportDidKey(), and whoever holds the private key can act as the actor, so store it with the actor and keep it safe:
import { generateCryptoKeyPair } from "@fedify/fedify";
import { exportDidKey } from "@fedify/vocab-runtime";
const { publicKey, privateKey } = await generateCryptoKeyPair("Ed25519");
const did = await exportDidKey(publicKey); // did:key:z6Mk...
// The verification method that the actor's proofs refer to:
const didKeyId = new URL(`${did}#${did.slice("did:key:".length)}`);
The actor dispatcher returns a portable actor as it would an ordinary one. Context.getPortableActorUri() and its siblings for inboxes, outboxes, collections, and objects build portable IDs from the dispatcher's paths and the DID, and signObject() signs the actor with the DID's key:
federation
.setActorDispatcher("/users/{identifier}", async (ctx, identifier) => {
const user = await findUser(identifier);
if (user == null) return null;
// A request through the gateway endpoint names the DID in its path,
// e.g., GET /.well-known/apgateway/did:key:z6Mk.../users/alice:
if (
ctx.portableRequest != null &&
ctx.portableRequest.authority !== user.did
) {
return null;
}
// This server's gateway keys for the actor (see below):
const keys = await ctx.getActorKeyPairs(identifier);
return await signObject(
new Person({
id: ctx.getPortableActorUri(identifier, user.did), // ap+ef61://did:key:z6Mk.../users/alice
preferredUsername: user.username,
inbox: ctx.getPortableInboxUri(identifier, user.did),
outbox: ctx.getPortableOutboxUri(identifier, user.did),
followers: ctx.getPortableFollowersUri(identifier, user.did),
gateways: [new URL("https://example.com")],
publicKeys: keys.map((key) => key.cryptographicKey),
assertionMethods: keys.map((key) => key.multikey),
}),
user.didKeyPair.privateKey,
new URL(`${user.did}#${user.did.slice("did:key:".length)}`),
);
})
.setKeyPairsDispatcher(async (ctx, identifier) => {
const user = await findUser(identifier);
// This server's own key pair for the actor, not the DID's:
return user == null ? [] : [user.gatewayKeyPair];
})
.mapPortableActorId(async (ctx, identifier) => {
const user = await findUser(identifier);
return user == null ? null : ctx.getPortableActorUri(identifier, user.did);
});
With this dispatcher, Fedify serves the actor at its compatible identifier, https://example.com/.well-known/apgateway/did:key:z6Mk.../users/alice, which is how other servers retrieve it. It serves it through WebFinger as @alice@example.com, since the handle's domain comes from the first gateway in gateways, and at its ordinary URL as before. On the gateway endpoint, Fedify serves a document only if its ID is the requested portable ID and it carries a proof made with a key of that DID.
The key pairs dispatcher and the new ActorCallbackSetters.mapPortableActorId() give the actor gateway keys. These are the server's own keys for the actor, and they sign only the HTTP requests the server makes on the actor's behalf, such as deliveries. They never sign the actor's activities or objects; only the DID's key does that. The actor document lists them under assertionMethods, as FEP-521a describes, so that other servers can verify the HTTP Signatures. See Dispatching the actor and Gateway keys of portable actors for the details.
Object and collection dispatchers serve portable objects and collections through the gateway endpoint in the same way, routed by the path after the DID. Two rules protect the actor's data. A non-public portable object is served only if the dispatcher has an authorization predicate, because FEP-ef61 forbids a gateway to show it to anyone outside its audience; the new RequestContext.isSignedByAudience() helps to write one. Without a predicate, a request for such an object gets 404 Not Found, as though the gateway did not store it. A deleted object is served as a signed Tombstone with 410 Gone. Resources that portable objects refer to by SHA-256 hashlinks, such as GET /.well-known/apgateway/hl:zQm..., are served by the new Federatable.setHashlinkMediaDispatcher().
A portable actor's activities have to be signed by its DID, which Fedify's key pairs dispatcher does not know about. Sign them with signObject() before sending, and pass normalizeExistingProofs: true so that the activity goes out in the form its proof covers. An activity that embeds a portable object, such as the Note of a Create, is signed inside out:
const actor = withGatewayHints(
ctx.getPortableActorUri(user.identifier, user.did),
[new URL("https://example.com")],
); // ap+ef61://did:key:z6Mk.../users/alice?@gateway=https%3A%2F%2Fexample.com
const note = await signObject(
new Note({
id: ctx.getPortableObjectUri(
Note,
{ identifier: user.identifier, id: crypto.randomUUID() },
user.did,
),
attribution: actor,
to: PUBLIC_COLLECTION,
content: "Hello, world!",
}),
user.didKeyPair.privateKey,
didKeyId,
);
const create = await signObject(
new Create({
id: ctx.getPortableObjectUri(
Note,
{ identifier: user.identifier, id: crypto.randomUUID() },
user.did,
),
actor,
to: PUBLIC_COLLECTION,
object: note,
}),
user.didKeyPair.privateKey,
didKeyId,
);
await ctx.sendActivity(
{ identifier: user.identifier },
"followers",
create,
{ normalizeExistingProofs: true },
);
This example depends on a fix to signObject() that is useful outside FEP-ef61 as well. Previously, a signed object assigned to a typed parent was rebuilt under the parent's JSON-LD context when the parent was serialized. The child kept its proofValue but lost the document its proof covered, so it no longer verified on its own. signObject() now captures the exact document it signed, and nested serialization embeds that document verbatim. The captured document is a snapshot: clone() does not carry it, so sign a clone again if it has to be embedded. The design is in #1044 and the fix in #1051.
Context.sendActivity() refuses a portable activity that is unsigned, signed by the wrong key, or carries a Linked Data Signature, and throws a TypeError before anything is queued. It also refuses a few shapes of compound document that Fedify's own inboxes would reject, such as a received signed Follow embedded in an Accept; refer to such an object by its ID instead. Deliveries to portable inboxes go through the recipient's gateways, or else the @gateway hints on the inbox, trying at most five of them until one accepts. Before this release, they failed with UrlError: Unsupported protocol: ap+ef61:. The rules are in Delivering to portable actors and Producing a compound document.
On the receiving side, a portable inbox is reached as POST /.well-known/apgateway/did:key:z6Mk.../users/alice/inbox and dispatched to the same inbox listeners as the ordinary inbox. Context.parseUri() gains a portable option, and the parsed result has the DID in a new authority property to check against the one you store. Fedify accepts an activity of a portable actor, or one with a portable ID, only if it has a valid proof by that DID; neither an HTTP Signature nor a Linked Data Signature authenticates it. Each portable object embedded in the activity is verified against its own proof before dispatch. An accepted activity that carries its own proof is forwarded to the actor's other gateways at most once, as FEP-ef61 recommends. Forwarding needs a key–value store that supports cas(), and the FederationOptions.portableInboxForwarding option tunes it. See Portable inboxes and Forwarding to other gateways.
Applications that never host a portable actor still meet portable objects from other servers. Context.lookupObject() resolves portable IDs, compatible identifiers, and the fediverse handles of portable actors, and returns portable objects only if they verify:
const actor = await ctx.lookupObject("@alice@example.com");
// A portable ID without @gateway hints needs gateways to ask:
const note = await ctx.lookupObject("ap://did:key:z6Mk.../notes/1", {
gateways: ["https://example.com"],
});
Property accessors fetch portable references through gateways as well, and they verify what they fetch with the new verifyPortableObject option. Context has a matching verifyPortableObject property, so passing a context as the options, as in await create.getObject(ctx), is enough. Activities received in inboxes use this verifier by default, and objects fetched through their accessors inherit it, so a chained call such as (await create.getObject(ctx))?.getAttribution() verifies each portable object along the way. A lookup asks at most five gateways, falling back to the next when one fails or serves something that does not verify. Unsigned collections are accepted only from a gateway that the owner lists, and only for an actor's inbox, outbox, followers, following, and liked, as FEP-ef61 allows. fetchPortableMedia() retrieves the media of a portable object and returns it only if it matches the object's digestMultibase. See Dereferencing portable references and Default verifiers.
Software that does not understand ap: URIs can refer to a portable object by a compatible identifier, an HTTP(S) URL under a gateway's /.well-known/apgateway/ path such as https://gw.example/.well-known/apgateway/did:key:z6Mk.../actor. Some implementations, tootik among them, use compatible identifiers as the IDs of their portable actors.
Earlier versions of Fedify treated these URLs as ordinary web URLs and trusted them by the server that served them. Since anyone can serve a compatible identifier for any DID, https://evil.example/.well-known/apgateway/did:key:z6MkAlice/actor could act as Alice without a proof by Alice's DID. Fedify 2.4.0 treats a compatible identifier as the portable object it stands for, as FEP-ef61 requires. Inboxes reject an activity from an actor whose ID is a compatible identifier with 401 Unauthorized unless it has a valid Object Integrity Proof by the actor's DID, and there is no option to turn this off. Actors that publish compatible identifiers without signing them are no longer accepted, and neither are those whose DIDs use key types that Fedify cannot verify yet. The change was tracked in #1093 and made in #1105.
Your own portable actors may still use compatible identifiers as their IDs, for software that rejects ap: IDs. Build them with toCompatibleEf61Id() before signing, and Fedify will treat them as portable wherever it serves or sends them. It also warns if one is malformed or does not point at the actor's first gateway. See Compatible identifiers as actor IDs.
Two choices in Fedify's profile differ from the current FEP text or rest on semantics that are not settled yet. Fedify keeps both for the 2.x series, and either may change in Fedify 3.0.
The first is the scheme. FEP-ef61 recommends ap: and canonicalizes ap+ef61: to it, while also warning that the recommendation may change to ap+ef61:, since these IDs are meant only for portable objects. Fedify accepts both, compares them as equal, and serializes ap+ef61:. Parsing a document normalizes its portable IRIs to ap+ef61:, so re-serializing an object that another implementation signed with ap: IDs breaks its proof. Keep the received JSON of such documents, and compare portable IDs with arePortableUrisEqual() rather than as strings. The discussion is in #826 and #828, and the reasoning is in Canonical ap+ef61: scheme.
The second concerns compound documents. When a signed Note sits inside a signed Create, the verifier has to know which part of the JSON each proof covers, and neither FEP-8b32 nor Verifiable Credential Data Integrity defines that yet; the question is open in w3c/vc-data-integrity#350. Fedify uses an interim map-local profile, worked out in #938. Each JSON map with a proof is its own secured document, each embedded portable object needs its own proof and its own @context, and proof sets and chains are not supported. The profile authenticates the exact JSON values each proof covers; it does not promise that a child's JSON-LD expansion inside its parent matches its expansion on its own. @silverpill, @dmitrizagidulin, and @by_caballero reviewed the proposal in that thread, and @silverpill reproduced its test vectors independently. See Map-local compound proofs.
Fedify does not implement FEP-ae97, the companion proposal for client-side signing. There are no endpoints for registering actors, submitting client-signed activities to a portable outbox, or gateway discovery, and no media upload and deletion endpoints under /.well-known/apgateway-media. Your own outbox listeners can still relay activities that a client signed, unchanged. Fedify also does not copy or reconcile actors, objects, or collections between gateways, so running an actor on several gateways means keeping their data in sync yourself. There is no workflow for rotating a DID's key or migrating an actor to another DID. Fedify resolves verification methods only for did:key DIDs with Ed25519 keys; other DID methods verify only if your document loader resolves them. Gateways must be plain origins, since the arbitrary gateway paths that FEP-ef61 discusses are not supported. The full list is in Not supported.
The profile was tested against tootik v0.25.4 and Mitra v5.10.0 in #1202. Fedify verifies tootik's portable actors, activities, and HTTP Signatures, and Mitra's gateway HTTP Signatures on activities that a client signed. Both accept the portable activities that Fedify sends. Two fixes came out of this testing. Fedify now reads a gateways list that tootik leaves unmapped in its JSON-LD context. It also serializes gateways as bare origins, because Mitra rejects a trailing slash. Fedify has not yet verified an actor or activity that Mitra signed itself, and Mastodon's handling of portable actors has not been tested. The FEP-ef61 interoperability section of FEDERATION.md records what was tested and how.
fedify lookup looks up portable objects by their ap: and ap+ef61: IDs, compatible identifiers, and the handles of portable actors, and it verifies their proofs. When no gateway returns an acceptable object, it says why. A new --gateway option on fedify lookup, fedify inbox, and fedify webfinger names the gateways to ask. fedify inbox --follow follows portable actors, and fedify webfinger accepts portable actor IDs. See Looking up portable objects in the CLI manual.
The mock federation and contexts in @fedify/testing support portable IDs, verification against fixture document loaders, gateway requests, and hashlink media, so tests can exercise portable objects without a live gateway. @fedify/lint's actor URI rules accept the portable IDs that the getPortable*Uri() methods build. @fedify/next now passes hashlink media requests to Fedify, a problem reported in #1149 and fixed in #1170; to have Next.js run the middleware for them, add { source: "/.well-known/apgateway/:path*" } to your matcher.
Fedify already runs activity delivery and inbox processing on queue workers, but applications usually have jobs of their own: digest emails, timeline rebuilds, link previews. Until now, those jobs could not use Fedify's queues, serialization, or retries. Fedify 2.4.0 opens the same machinery to application-defined tasks.
A task is defined once with defineTask() and enqueued from any Context with enqueueTask() or enqueueTaskMany(). Every task takes a Standard Schema, such as a Zod or Valibot schema, which types its payload. Fedify validates the payload when it is enqueued and again when a worker picks it up; the second check catches payloads from an earlier deployment that no longer match the schema:
import { createExponentialBackoffPolicy } from "@fedify/fedify";
import { z } from "zod";
const sendDigest = federation.defineTask("sendDigest", {
schema: z.object({ userId: z.string(), since: z.date() }),
handler: async (ctx, data) => {
// data is typed as { userId: string; since: Date }
await sendEmail(data.userId, await buildDigest(data.userId, data.since));
},
retryPolicy: createExponentialBackoffPolicy({ maxAttempts: 3 }),
});
// Anywhere with a Context:
await ctx.enqueueTask(sendDigest, { userId: "alice", since: new Date() }, {
delay: { minutes: 30 },
deduplicationKey: "digest:alice",
});
Fedify serializes payloads with devalue, so Date, Map, Set, URL, bigint, Temporal values, circular references, and Activity Vocabulary objects all survive the trip through any message queue backend. A Note in a payload comes back as a Note. Failed handlers are retried with exponential backoff by default, and queues with native retries handle retries themselves. Tasks fall back to the outbox queue unless you give them their own. Use the new task slot of the queue option or a per-task queue for that, and startQueue(ctxData, { queue: "task" }) runs a task-only worker. Setting taskQueueResolution: "strict" turns the fallback into an error.
A deduplicationKey deduplicates enqueues. The feature came from @julian's point in #206 that recurring tasks are hard to manage when you cannot tell whether one is already queued. A queue that declares nativeDeduplication enforces the key itself; otherwise, Fedify keeps a best-effort marker in the key–value store through cas(). Each task runs in a fedify.task span, and the queue metrics from 2.3.0 now include task queues. The Background tasks chapter covers routing, retries, deduplication, and the limitations; cron-style scheduling, result backends, and priorities are out of scope for now.
ChanHaeng Lee (@2chanhaeng) implemented the API: the core in #803, deduplication in #806, and observability in #812, following the plans in #797, #798, and #799. The work was merged in #923, closing #206, which had been open since February 2025.
Fedify 2.2.0 added client-to-server outbox listeners, but a C2S client also has to upload images and video before it can post them. That traffic does not go through POST /outbox. Fedify 2.4.0 supports the ActivityPub Media Upload extension, a SocialCG proposal last updated in 2017 that never reached W3C Recommendation status.
setMediaUploader() registers a multipart/form-data endpoint. Its callback receives the uploaded file and the object shell the client posted along with it. If the callback returns the created object, Fedify responds with 201 Created; if it returns a URL where the object will appear once processing finishes, Fedify responds with 202 Accepted:
federation
.setMediaUploader(
"/users/{identifier}/media",
async (ctx, identifier, file, object) => {
const stored = await uploadToStorage(file);
return new Image({
id: ctx.getObjectUri(Image, { uuid: stored.uuid }),
url: new URL(stored.publicUrl),
mediaType: file.type,
name: object.name,
});
},
)
.authorize(async (ctx, identifier) => {
const session = await verifyAccessToken(
ctx.request.headers.get("authorization"),
);
return session?.identifier === identifier;
});
The actor dispatcher advertises the endpoint itself by setting endpoints: new Endpoints({ uploadMedia: ctx.getMediaUploaderUri(identifier) }), the same way it places its inbox and outbox URIs. Without an authorize() hook, anyone who finds the endpoint can upload to it. For that reason, Fedify logs a warning when an uploader is registered without one, and when the returned object's URI does not match an object dispatcher or the actor does not advertise the endpoint. Four new @fedify/lint rules catch the same mistakes before they run. Fedify reads the upload into memory and sets no size limit of its own, so cap the request size at your reverse proxy. Storing the file and serving the object back are up to the application. See the Media upload chapter; the work is in #754 and #927.
Fedify decides several things about every inbox delivery: which signature mechanisms applied, which keys they tried, whether the actor owned the key, and what happened to the activity. Before this release, applications saw only part of that. A Linked Data Signature by a key the actor did not own was counted the same as a bad signature, and the key that verified a request was never handed to the application. The request in #1191 came from an effort to log these decisions for a debugging tool.
The new onRequestFinished() callback on inbox listeners receives an InboxRequestReport for every delivery to an inbox route, including rejected requests and requests that fail before verification:
federation
.setInboxListeners("/users/{identifier}/inbox", "/inbox")
.on(Follow, async (ctx, follow) => { /* ... */ })
.onRequestFinished(async (ctx, report) => {
await db.insertInboxLog({
inbox: report.inbox.kind, // "personal" | "shared" | "portable"
authentication: report.authentication.status,
outcome: report.outcome.type === "response"
? report.outcome.disposition // "processed" | "enqueued" | "duplicate" | ...
: `exception:${report.outcome.stage}`,
keyIds: report.attempts
.flatMap((attempt) => attempt.checks)
.flatMap((check) => check.triedKeys)
.map((key) => key.id?.href),
});
});
Each HTTP Signature, Linked Data Signature, or Object Integrity Proof check appears as an attempt with the actual keys it tried, including a stale cached key and its refreshed replacement. The report also carries the final authentication decision, such as actorKeyMismatch or proofPolicy, and the response or exception that ended the request. Fedify awaits the callback whether or not the trace is sampled. Errors it throws are logged without changing the response, and enqueued means the queue accepted the activity, not that a worker later processed it. Mock federations in @fedify/testing accept the same callback. See Observing inbox requests; the implementation is in #1201.
Actor dispatchers have been able to return a Tombstone since 2.2.0, but object dispatchers could only return null. A deleted post was answered with 404 Not Found, as if it had never existed, so applications that wanted 410 Gone, as ActivityPub recommends, needed their own route in front of Fedify.
Object dispatchers may now return a Tombstone, which Fedify serves with 410 Gone and the serialized tombstone after applying the authorization predicate:
federation.setObjectDispatcher(
Note,
"/users/{identifier}/notes/{id}",
async (ctx, values) => {
const post = await getPost(values.identifier, values.id);
if (post == null) return null;
if (post.deletedAt != null) {
return new Tombstone({
id: ctx.getObjectUri(Note, values),
formerType: Note,
deleted: post.deletedAt,
});
}
return new Note({ id: ctx.getObjectUri(Note, values), content: post.content });
},
);
The return type of ObjectDispatcher widens to TObject | Tombstone | null. RequestContext.getObject() still returns null for a tombstone unless you pass { tombstone: "passthrough" } or ask for the Tombstone or Object class itself. Dispatchers registered for Tombstone or Object that already returned tombstones now respond with 410 rather than 200. See Deleted objects; the change was proposed in #1112 and made in #1117.
Much of what Fedify fetches is chosen by the sender of an incoming request. Key IDs in signatures are one example, and the servers behind them can be slow on purpose. Several changes in 2.4.0 bound that work.
As #1130 points out, a request may carry several RFC 9421 signatures, and each one can make Fedify fetch a key from a URL of the sender's choosing. Fedify now verifies at most the first three, in Signature-Input order, and ignores the rest. A signature counts toward the limit even if it fails before its key is fetched, and a key named by several signatures is looked up once. The new FederationOptions.maxHttpSignatures option, added with the limit in #1166, changes it.
The built-in document loaders now give each call ten seconds by default, a limit proposed in #1131 and added in #1169. The time limit covers every redirect, alternate document, double-knocking retry, and the response body. A timed-out call throws a FetchError whose cause is a DOMException named "TimeoutError". Error responses are read in full before the loader throws, up to 1 MiB, so that a server cannot send a 404 header and then stall the body. FederationOptions.documentLoaderTimeout changes the limit or turns it off with null, and custom document loaders are not affected. Since #913, getDocumentLoader() also rejects HTML responses that do not advertise an ActivityPub alternate with a FetchError, rather than failing inside JSON.parse(), which #912 found cluttering error trackers.
Cached public keys and remembered HTTP Message Signatures specs used to live forever in the key–value store. #1017 reports a production Redis instance holding 194,424 public-key entries, about 165 MiB, for actors that had mostly stopped federating. Keys now expire after 30 days and specs after 90, configurable through FederationOptions.publicKeyTtl and FederationOptions.httpMessageSignaturesSpecTtl. Entries written by earlier versions have no expiry until they are next written. The Clearing legacy cache entries section shows how to remove them for Redis, PostgreSQL, and other stores. Heewon Chae (@heeoneie) implemented this in #1027, their first contribution to Fedify.
const federation = createFederation<void>({
kv,
maxHttpSignatures: 1, // default: 3
documentLoaderTimeout: { seconds: 5 }, // default: 10 seconds
publicKeyTtl: { days: 7 }, // default: 30 days
httpMessageSignaturesSpecTtl: { days: 30 }, // default: 90 days
});
Finally, verifyProof() now authenticates every option of an Ed25519 JCS proof except proofValue, including expires, domain, challenge, and nonce. It also rejects expired or malformed ones. Previously, a field such as expires could be added to an already signed proof without invalidating it. Jiwon Kwon (@z9mb1) spotted this in review of #968. Callers can now require a domain and challenge through VerifyProofOptions to prevent cross-domain or replayed use.
@fedify/adonisjs@fedify/adonisjs integrates Fedify with AdonisJS. Running node ace add @fedify/adonisjs registers a service provider that owns the federation's lifecycle and a server middleware that mounts it. It also scaffolds config/fedify.ts, start/federation.ts, and app/federation/main.ts. Controllers get a ctx.federation request context:
// app/federation/main.ts
import federation from "@fedify/adonisjs/services/builder";
import { Person } from "@fedify/vocab";
federation.setActorDispatcher("/actors/{identifier}", (ctx, identifier) =>
new Person({ id: ctx.getActorUri(identifier), preferredUsername: identifier }));
The middleware sits first in AdonisJS's server stack rather than its router stack. That way Fedify sees paths the router does not know, such as /.well-known/webfinger, and reads request bodies before the body parser consumes them. The package targets Node.js and is published to npm only. See the AdonisJS section of the integration guide.
Samuel Brinkmann (@sabrinkmann) contributed the package in #1006, closing #139, and built it on a prototype that Emelia Smith (@thisismissem) attached to that issue.
npm add @fedify/adonisjs
pnpm add @fedify/adonisjs
yarn add @fedify/adonisjs
@fedify/interaction-controlsGoToSocial interaction controls, FEP-044f quote authorization, and FEP-7aa9 featured collections share a request-and-authorization flow. A server asks to reply to, quote, or feature a post, and the author's server approves according to the post's interactionPolicy. @fedify/interaction-controls implements that flow for like, reply, announce, quote, and feature interactions. Each has a helper, such as likeInteraction or quoteInteraction, that creates and verifies requests and authorizations, evaluates policies, and recognizes interactions that arrived without a request:
import { likeInteraction } from "@fedify/interaction-controls";
// When a LikeRequest arrives:
const verified = await likeInteraction.verifyRequest(ctx, { request });
if (verified.verified) {
const decision = await likeInteraction.evaluatePolicy(ctx, {
subject: note,
requester: request.actorId!,
});
if (decision.result === "automatic") { // or "manual" or "denied"
const authorization = likeInteraction.createAuthorization({
id: new URL("https://example.com/authorizations/1"),
attributedTo: owner,
interactingObject: like,
interactionTarget: note,
});
// ...
}
}
The package does not install inbox listeners or store anything; the application decides where requests and authorizations live and when a human needs to approve. Verification fails closed. Without a policy, likes, replies, and announces are approved automatically, while quotes and features are denied. @fedify/vocab gains the FEP-7aa9 types this needs: FeaturedCollection, FeaturedItem, FeatureRequest, and FeatureAuthorization, along with actors' featuredCollections and InteractionPolicy.canFeature. Samuel Brinkmann asked for them in #810 once Mastodon 4.6 shipped the FEP. See the Interaction controls chapter; the work is in #914 and #929.
npm add @fedify/interaction-controls
pnpm add @fedify/interaction-controls
yarn add @fedify/interaction-controls
deno add jsr:@fedify/interaction-controls
bun add @fedify/interaction-controls
@fedify/netlify@fedify/netlify lets a Fedify application run on Netlify Functions without an external database or queue. NetlifyMessageQueue submits jobs as Netlify Async Workloads events, and createNetlifyQueueHandler() turns a Function into their consumer, with delayed delivery, native retries, and per-key FIFO ordering. NetlifyBlobsKvStore is a key–value store on Netlify Blobs with expiration, prefix listing, and compare-and-swap:
import {
createNetlifyQueueHandler,
NetlifyBlobsKvStore,
NetlifyMessageQueue,
} from "@fedify/netlify";
import { AsyncWorkloadsClient } from "@netlify/async-workloads";
import { getStore } from "@netlify/blobs";
export const kv = new NetlifyBlobsKvStore(
getStore({ name: "fedify", consistency: "strong" }),
);
export const queue = new NetlifyMessageQueue({
client: new AsyncWorkloadsClient(),
orderingKv: kv,
});
// netlify/functions/fedify-queue.ts
export default createNetlifyQueueHandler({
queue,
maxRetries: 4,
federation: () => builder.build({ kv, queue, manuallyStartQueue: true }),
});
The key–value store exists because of cost. #1010 describes a static blog whose ActivityPub traffic kept a Netlify Database from ever idling, which used most of the account's monthly credits. Jiwon Kwon contributed NetlifyBlobsKvStore in #1029, and the queue landed in #934. See Netlify Functions in the deployment guide.
npm add @fedify/netlify
pnpm add @fedify/netlify
yarn add @fedify/netlify
deno add jsr:@fedify/netlify
bun add @fedify/netlify
@fedify/pglite@fedify/pglite provides PgliteKvStore, a KvStore on an embedded PGlite database that you create and pass in. It uses the same schema as PostgresKvStore. It is meant for a single PGlite instance in a single runtime isolate, so it does not include a message queue: PGlite does not share data between processes.
import { PGlite } from "@electric-sql/pglite";
import { PgliteKvStore } from "@fedify/pglite";
const federation = createFederation({
kv: new PgliteKvStore(new PGlite("./data/fedify")),
});
ChanHaeng Lee contributed the package in #1020, together with testKvStore() in @fedify/testing. testKvStore() is a conformance suite for KvStore implementations, the counterpart to testMessageQueue(). See PgliteKvStore.
npm add @fedify/pglite
pnpm add @fedify/pglite
yarn add @fedify/pglite
deno add jsr:@fedify/pglite
bun add @fedify/pglite
Portable inbox forwarding, task deduplication, Netlify queue ordering, and the delivery circuit breaker all need an atomic compare-and-swap on the key–value store. PostgresKvStore now implements cas(), added in #934, and so does RedisKvStore, requested in #1163 and added in #1167. The Redis version uses a single-key Lua script, which works on standalone Redis and Redis Cluster. Before this, the circuit breaker fell back to racy updates on Redis.
Both come with conditions. RedisKvStore needs Redis to permit EVAL, and custom codecs must encode equal values identically. PostgresKvStore now creates logged tables by default, because losing sequence state in crash recovery could block queued events for good. It migrates existing unlogged tables when it initializes. That one-time migration rewrites the table and locks it exclusively, so schedule the upgrade accordingly if the table is large or busy; unlogged: true keeps the old behavior.
fedify init and the CLIfedify init can now scaffold a SvelteKit project, contributed by Jang Hanarae (@menele) in #971. Jang Hanarae also added a test task to generated projects in #990. It starts the app, waits for it to be ready, and checks that it resolves a local actor, which gives new projects a smoke test to run after scaffolding and after later changes. Before generating anything, fedify init now checks that the selected Deno, Bun, or Node.js meets Fedify's minimum version, or a framework's higher one, such as Astro's Node.js 22.12. It fails with a clear error in non-interactive mode, and in interactive mode it disables the affected package managers. Lee Jeongmin (@userjmmm) contributed the check in #981. Astro projects now use Astro 7, and since #936 @fedify/astro is tested against Astro 5, 6, and 7 with real builds on Node.js, Deno, and Bun, as #931 asked.
fedify tunnel gains fedify.com.es, a tunneling service run by the Fedify project that needs no account; the CLI pins its SSH host key and refuses a mismatched server before exposing a local port. localhost.run is removed in the same change, #940, because the service no longer exists. fedify lookup now times out each request after ten seconds when -T/--timeout is not given, and the option now also bounds the wait for a DNS lookup. The CLI moves to Optique 1.3.2 in #1197, which handles attached values like --timeout=30 consistently, gives better suggestions for mistyped options, and no longer hides errors for known options behind positional arguments before --.
@fedify/lint gains several rules beyond the media upload ones mentioned above. actor-preferred-username-required, requested in #895 and added in #1022, warns when an actor dispatcher returns an actor without preferredUsername. outbox-listener-delivery-not-awaited, from #1057 and #1067, reports an outbox listener that calls ctx.sendActivity() or ctx.forwardActivity() and drops the promise. On Cloudflare Workers, such an activity may never leave, because pending work is discarded once the response returns. await, return, Promise.all(), Promise.allSettled(), and waitUntil() count as handling the promise, while void, Promise.race(), and Promise.any() are accepted as a deliberate choice not to wait. The rule is a warning in the ESLint recommended configuration and an error in strict. Oxlint users enable it by name, and it is not available in Deno Lint.
In #1050, which resolves #900, the existing outbox-listener-delivery-required rule began following control flow instead of scanning the listener's source as text. It reports a listener whose only delivery calls sit in a dead branch, after an unconditional return or throw, or in a function that is never used, and it stays quiet when it cannot tell. Jae-Hyuk-Jang (@jhyuk) contributed this change and both new rules. archie (@ArchieTansaria) then taught both delivery rules about statically false loops and loop binding patterns in #1088. See the linting guide.
@fedify/vocab supports the FEP-22cd draft for translations. A Translation records a translated version's language, translators, source object, and source review time, and the new Object.translations property attaches them to a post without creating separate posts. See Translation metadata. (#1037, #1038)@fedify/vocab-tools gains an extraContext property option, which adds a JSON-LD context only when its terms are used, and a trustEmbeddedObjects type option. (#1037, #1038)suppressError: true handles are now logged as warnings rather than errors, so that they no longer show up as application errors. Contributed by Jae Hui Hong (@axz0612). (#933, #1035)enqueueMany() makes separate sends can set MessageQueue.atomicEnqueueMany to false. Fedify then rejects a multi-message batch governed by one deduplicationKey up front, since a partial send followed by a retry could enqueue duplicates. (#930, #934)Temporal bundle temporal-polyfill instead of requiring @js-temporal/polyfill at runtime, and type declarations rely on the standard esnext.temporal lib. (#823, #925)Accept header now includes the ActivityStreams profile on the JSON-LD media type, as ActivityPub and FEP-ef61 gateways require. (#834, #1077)fedify lookup --recurse reporting a timeout or other network failure as a possibly private object; it now reports the actual cause. (#1156, #1199)-p/--allow-private-address option of fedify webfinger being ignored. (#1156, #1199)outbox-listener-delivery-required and outbox-listener-delivery-not-awaited losing track of an object's functions when a nested helper assigns another property of that object. (#1125, #1140)npm add @fedify/fedify@2.4.0
pnpm add @fedify/fedify@2.4.0
yarn add @fedify/fedify@2.4.0
deno add jsr:@fedify/fedify@2.4.0
bun add @fedify/fedify@2.4.0
Update the other @fedify/* packages you use to 2.4.0 at the same time. To update the CLI, run npm install -g @fedify/cli@2.4.0, or see the installation instructions for other package managers.
Most applications need no code changes, but these changes in behavior may affect you:
401 Unauthorized unless they carry a valid Object Integrity Proof by the actor's DID. Keys at compatible identifiers, and keys at ordinary URLs that claim a portable owner, are no longer trusted by origin.Context as their options, such as await create.getObject(ctx), now verify the portable objects they dereference, including references to compatible identifiers from objects with ordinary HTTP(S) IDs./.well-known/apgateway/, and Context.sendActivity() may reject an activity that embeds others' portable objects. Next.js applications need the matcher shown above to serve hashlink media.maxHttpSignatures if you need more.fedify lookup does too.Tombstone or Object that return tombstones now respond with 410 Gone instead of 200 OK.PostgresKvStore migrates existing unlogged tables to logged tables on initialization, which locks the table.RedisKvStore needs EVAL permission for cas().Context and RequestContext interfaces need the new getPortable*Uri() methods and isSignedByAudience().fedify tunnel no longer supports localhost.run.Thanks to this release's contributors:
@fedify/adonisjs, and the request for FEP-7aa9 vocabularytest task in fedify initfedify initactor-preferred-username-required and outbox-listener-delivery-not-awaited lint rules, and control-flow analysis for outbox-listener-delivery-requiredChanHaeng Lee (@2chanhaeng) and Jiwon Kwon (@z9mb1) joined as maintainers after 2.3.0. ChanHaeng built the background task API and @fedify/pglite, Jiwon built NetlifyBlobsKvStore and rewrote the relay documentation, and both reviewed much of the rest of this release.
Many people added tests, documentation, and infrastructure during this cycle, often starting from a good first issue: Seoa Yoon (@bananamilk452), Lim Kyoujin (@dktsudgg), @m2nsp, Cho Seongmin (@seongmin36), Safal Shrestha (@Safal-Shrestha-SS), Jo Youngjae (@jaonz6057), @kecan0406, Loi Nguyen (@lntutor), Oh Taejun (@0w0), Yerin Park (@dstforye), Lee Dogeon (@moreal), and Sae Jin Kim (@siliconsjang).
The FEP-ef61 work owes a lot to people outside the project. @anotherdoesnm opened the request in #288. @silverpill, the author of FEP-ef61 and FEP-ae97, clarified the gateway trust model, reviewed the compound proof profile, and checked its test vectors. @dmitrizagidulin and @by_caballero weighed in on the profile's JSON-LD questions in #938. Testing against tootik and Mitra led to two of the interoperability fixes in this release.
See CHANGES.md for the complete changelog.
github.com
ActivityPub server framework in TypeScript. Contribute to fedify-dev/fedify development by creating an account on GitHub.
Fedify 2.4.0 is out.
The main addition is support for FEP-ef61 portable objects. A Fedify server can now host actors whose identity is a DID instead of a domain, serve their signed objects through /.well-known/apgateway/, and verify the portable objects of other servers. I tested it against tootik and Mitra, and wrote up where Fedify's profile differs from the FEP and what it leaves out.
The release also adds application-defined background tasks, the ActivityPub Media Upload extension, per-request inbox reports, 410 Gone for deleted objects, and four new packages (AdonisJS, Netlify, PGlite, and interaction controls).
Some defaults changed, so please read the upgrading section before you update, especially if you federate with software that uses FEP-ef61 compatible identifiers.
Release notes: https://hackers.pub/@fedify/2026/fedify-2-4.
Thanks to everyone who contributed to this cycle, and to @silverpill for patient answers about the FEP.
hackers.pub
Fedify is a TypeScript framework for building ActivityPub servers. It implements federation details such as HTTP Signatures, JSON-LD processing, WebFinger, inbox and outbox routing, and activity delivery.
Fedify 2.4.0 is a minor release with an unusually long changelog. The main addition is support for FEP-ef61 portable objects: a Fedify server can now host actors whose identity is a DID rather than a domain, serve and verify their signed objects, and consume the portable objects of other servers. The release also adds an API for application-defined background tasks, the ActivityPub Media Upload extension, a hook that reports how every inbox request was authenticated and handled, and 410 Gone tombstones for deleted objects. Several defaults change to limit what a hostile peer can make Fedify do. Four new packages ship with this version: @fedify/adonisjs, @fedify/interaction-controls, @fedify/netlify, and @fedify/pglite.
Most applications can upgrade without code changes, but a few of the new defaults are behavior changes. They are listed in the Upgrading section at the end.
An ActivityPub object's ID is an HTTP(S) URL on its server's domain. If that server shuts down, its IDs stop resolving, and an actor that moves elsewhere gets a new ID that everyone has to follow again. FEP-ef61, written by @silverpill of Mitra, proposes IDs that do not name a server at all. A portable ID is an ap: URI whose authority is a DID:
ap://did:key:z6MkrJVnaZkeFzdQyMZu1cgjg7k1pZZ6pvBQ7XJPt4swbTQ2/actor
The same object can be stored on several servers, called gateways, each of which serves it under /.well-known/apgateway/. Because no server owns the ID, a portable actor, activity, or object is authenticated only by its FEP-8b32 Object Integrity Proof, made with a key of the DID in its ID. A portable actor lists its gateways in a gateways property, and other servers deliver to its inbox through them.
Fedify 2.4.0 implements a profile of FEP-ef61 for gateways and consumers of portable objects. It makes two deliberate choices where the FEP or the specifications it rests on leave questions open, and it leaves out a few parts of the proposal; both are described below. The new Portable objects chapter of the manual walks through running portable actors from start to finish and then describes the profile as a whole, so that other implementers can tell what to expect from a Fedify server. The request came from @anotherdoesnm in #288, which tracked the work from a roadmap to the final pieces.
Portable objects are a foundation for nomadic identity, not nomadic identity itself. Fedify serves and verifies portable objects and forwards activities between gateways, but it does not copy actors or posts between servers, rotate keys, or move an actor to another DID. Full nomadic identity is still open in #413.
A portable actor starts with an Ed25519 key pair. Its DID is made from the public key with exportDidKey(), and whoever holds the private key can act as the actor, so store it with the actor and keep it safe:
import { generateCryptoKeyPair } from "@fedify/fedify";
import { exportDidKey } from "@fedify/vocab-runtime";
const { publicKey, privateKey } = await generateCryptoKeyPair("Ed25519");
const did = await exportDidKey(publicKey); // did:key:z6Mk...
// The verification method that the actor's proofs refer to:
const didKeyId = new URL(`${did}#${did.slice("did:key:".length)}`);
The actor dispatcher returns a portable actor as it would an ordinary one. Context.getPortableActorUri() and its siblings for inboxes, outboxes, collections, and objects build portable IDs from the dispatcher's paths and the DID, and signObject() signs the actor with the DID's key:
federation
.setActorDispatcher("/users/{identifier}", async (ctx, identifier) => {
const user = await findUser(identifier);
if (user == null) return null;
// A request through the gateway endpoint names the DID in its path,
// e.g., GET /.well-known/apgateway/did:key:z6Mk.../users/alice:
if (
ctx.portableRequest != null &&
ctx.portableRequest.authority !== user.did
) {
return null;
}
// This server's gateway keys for the actor (see below):
const keys = await ctx.getActorKeyPairs(identifier);
return await signObject(
new Person({
id: ctx.getPortableActorUri(identifier, user.did), // ap+ef61://did:key:z6Mk.../users/alice
preferredUsername: user.username,
inbox: ctx.getPortableInboxUri(identifier, user.did),
outbox: ctx.getPortableOutboxUri(identifier, user.did),
followers: ctx.getPortableFollowersUri(identifier, user.did),
gateways: [new URL("https://example.com")],
publicKeys: keys.map((key) => key.cryptographicKey),
assertionMethods: keys.map((key) => key.multikey),
}),
user.didKeyPair.privateKey,
new URL(`${user.did}#${user.did.slice("did:key:".length)}`),
);
})
.setKeyPairsDispatcher(async (ctx, identifier) => {
const user = await findUser(identifier);
// This server's own key pair for the actor, not the DID's:
return user == null ? [] : [user.gatewayKeyPair];
})
.mapPortableActorId(async (ctx, identifier) => {
const user = await findUser(identifier);
return user == null ? null : ctx.getPortableActorUri(identifier, user.did);
});
With this dispatcher, Fedify serves the actor at its compatible identifier, https://example.com/.well-known/apgateway/did:key:z6Mk.../users/alice, which is how other servers retrieve it. It serves it through WebFinger as @alice@example.com, since the handle's domain comes from the first gateway in gateways, and at its ordinary URL as before. On the gateway endpoint, Fedify serves a document only if its ID is the requested portable ID and it carries a proof made with a key of that DID.
The key pairs dispatcher and the new ActorCallbackSetters.mapPortableActorId() give the actor gateway keys. These are the server's own keys for the actor, and they sign only the HTTP requests the server makes on the actor's behalf, such as deliveries. They never sign the actor's activities or objects; only the DID's key does that. The actor document lists them under assertionMethods, as FEP-521a describes, so that other servers can verify the HTTP Signatures. See Dispatching the actor and Gateway keys of portable actors for the details.
Object and collection dispatchers serve portable objects and collections through the gateway endpoint in the same way, routed by the path after the DID. Two rules protect the actor's data. A non-public portable object is served only if the dispatcher has an authorization predicate, because FEP-ef61 forbids a gateway to show it to anyone outside its audience; the new RequestContext.isSignedByAudience() helps to write one. Without a predicate, a request for such an object gets 404 Not Found, as though the gateway did not store it. A deleted object is served as a signed Tombstone with 410 Gone. Resources that portable objects refer to by SHA-256 hashlinks, such as GET /.well-known/apgateway/hl:zQm..., are served by the new Federatable.setHashlinkMediaDispatcher().
A portable actor's activities have to be signed by its DID, which Fedify's key pairs dispatcher does not know about. Sign them with signObject() before sending, and pass normalizeExistingProofs: true so that the activity goes out in the form its proof covers. An activity that embeds a portable object, such as the Note of a Create, is signed inside out:
const actor = withGatewayHints(
ctx.getPortableActorUri(user.identifier, user.did),
[new URL("https://example.com")],
); // ap+ef61://did:key:z6Mk.../users/alice?@gateway=https%3A%2F%2Fexample.com
const note = await signObject(
new Note({
id: ctx.getPortableObjectUri(
Note,
{ identifier: user.identifier, id: crypto.randomUUID() },
user.did,
),
attribution: actor,
to: PUBLIC_COLLECTION,
content: "Hello, world!",
}),
user.didKeyPair.privateKey,
didKeyId,
);
const create = await signObject(
new Create({
id: ctx.getPortableObjectUri(
Note,
{ identifier: user.identifier, id: crypto.randomUUID() },
user.did,
),
actor,
to: PUBLIC_COLLECTION,
object: note,
}),
user.didKeyPair.privateKey,
didKeyId,
);
await ctx.sendActivity(
{ identifier: user.identifier },
"followers",
create,
{ normalizeExistingProofs: true },
);
This example depends on a fix to signObject() that is useful outside FEP-ef61 as well. Previously, a signed object assigned to a typed parent was rebuilt under the parent's JSON-LD context when the parent was serialized. The child kept its proofValue but lost the document its proof covered, so it no longer verified on its own. signObject() now captures the exact document it signed, and nested serialization embeds that document verbatim. The captured document is a snapshot: clone() does not carry it, so sign a clone again if it has to be embedded. The design is in #1044 and the fix in #1051.
Context.sendActivity() refuses a portable activity that is unsigned, signed by the wrong key, or carries a Linked Data Signature, and throws a TypeError before anything is queued. It also refuses a few shapes of compound document that Fedify's own inboxes would reject, such as a received signed Follow embedded in an Accept; refer to such an object by its ID instead. Deliveries to portable inboxes go through the recipient's gateways, or else the @gateway hints on the inbox, trying at most five of them until one accepts. Before this release, they failed with UrlError: Unsupported protocol: ap+ef61:. The rules are in Delivering to portable actors and Producing a compound document.
On the receiving side, a portable inbox is reached as POST /.well-known/apgateway/did:key:z6Mk.../users/alice/inbox and dispatched to the same inbox listeners as the ordinary inbox. Context.parseUri() gains a portable option, and the parsed result has the DID in a new authority property to check against the one you store. Fedify accepts an activity of a portable actor, or one with a portable ID, only if it has a valid proof by that DID; neither an HTTP Signature nor a Linked Data Signature authenticates it. Each portable object embedded in the activity is verified against its own proof before dispatch. An accepted activity that carries its own proof is forwarded to the actor's other gateways at most once, as FEP-ef61 recommends. Forwarding needs a key–value store that supports cas(), and the FederationOptions.portableInboxForwarding option tunes it. See Portable inboxes and Forwarding to other gateways.
Applications that never host a portable actor still meet portable objects from other servers. Context.lookupObject() resolves portable IDs, compatible identifiers, and the fediverse handles of portable actors, and returns portable objects only if they verify:
const actor = await ctx.lookupObject("@alice@example.com");
// A portable ID without @gateway hints needs gateways to ask:
const note = await ctx.lookupObject("ap://did:key:z6Mk.../notes/1", {
gateways: ["https://example.com"],
});
Property accessors fetch portable references through gateways as well, and they verify what they fetch with the new verifyPortableObject option. Context has a matching verifyPortableObject property, so passing a context as the options, as in await create.getObject(ctx), is enough. Activities received in inboxes use this verifier by default, and objects fetched through their accessors inherit it, so a chained call such as (await create.getObject(ctx))?.getAttribution() verifies each portable object along the way. A lookup asks at most five gateways, falling back to the next when one fails or serves something that does not verify. Unsigned collections are accepted only from a gateway that the owner lists, and only for an actor's inbox, outbox, followers, following, and liked, as FEP-ef61 allows. fetchPortableMedia() retrieves the media of a portable object and returns it only if it matches the object's digestMultibase. See Dereferencing portable references and Default verifiers.
Software that does not understand ap: URIs can refer to a portable object by a compatible identifier, an HTTP(S) URL under a gateway's /.well-known/apgateway/ path such as https://gw.example/.well-known/apgateway/did:key:z6Mk.../actor. Some implementations, tootik among them, use compatible identifiers as the IDs of their portable actors.
Earlier versions of Fedify treated these URLs as ordinary web URLs and trusted them by the server that served them. Since anyone can serve a compatible identifier for any DID, https://evil.example/.well-known/apgateway/did:key:z6MkAlice/actor could act as Alice without a proof by Alice's DID. Fedify 2.4.0 treats a compatible identifier as the portable object it stands for, as FEP-ef61 requires. Inboxes reject an activity from an actor whose ID is a compatible identifier with 401 Unauthorized unless it has a valid Object Integrity Proof by the actor's DID, and there is no option to turn this off. Actors that publish compatible identifiers without signing them are no longer accepted, and neither are those whose DIDs use key types that Fedify cannot verify yet. The change was tracked in #1093 and made in #1105.
Your own portable actors may still use compatible identifiers as their IDs, for software that rejects ap: IDs. Build them with toCompatibleEf61Id() before signing, and Fedify will treat them as portable wherever it serves or sends them. It also warns if one is malformed or does not point at the actor's first gateway. See Compatible identifiers as actor IDs.
Two choices in Fedify's profile differ from the current FEP text or rest on semantics that are not settled yet. Fedify keeps both for the 2.x series, and either may change in Fedify 3.0.
The first is the scheme. FEP-ef61 recommends ap: and canonicalizes ap+ef61: to it, while also warning that the recommendation may change to ap+ef61:, since these IDs are meant only for portable objects. Fedify accepts both, compares them as equal, and serializes ap+ef61:. Parsing a document normalizes its portable IRIs to ap+ef61:, so re-serializing an object that another implementation signed with ap: IDs breaks its proof. Keep the received JSON of such documents, and compare portable IDs with arePortableUrisEqual() rather than as strings. The discussion is in #826 and #828, and the reasoning is in Canonical ap+ef61: scheme.
The second concerns compound documents. When a signed Note sits inside a signed Create, the verifier has to know which part of the JSON each proof covers, and neither FEP-8b32 nor Verifiable Credential Data Integrity defines that yet; the question is open in w3c/vc-data-integrity#350. Fedify uses an interim map-local profile, worked out in #938. Each JSON map with a proof is its own secured document, each embedded portable object needs its own proof and its own @context, and proof sets and chains are not supported. The profile authenticates the exact JSON values each proof covers; it does not promise that a child's JSON-LD expansion inside its parent matches its expansion on its own. @silverpill, @dmitrizagidulin, and @by_caballero reviewed the proposal in that thread, and @silverpill reproduced its test vectors independently. See Map-local compound proofs.
Fedify does not implement FEP-ae97, the companion proposal for client-side signing. There are no endpoints for registering actors, submitting client-signed activities to a portable outbox, or gateway discovery, and no media upload and deletion endpoints under /.well-known/apgateway-media. Your own outbox listeners can still relay activities that a client signed, unchanged. Fedify also does not copy or reconcile actors, objects, or collections between gateways, so running an actor on several gateways means keeping their data in sync yourself. There is no workflow for rotating a DID's key or migrating an actor to another DID. Fedify resolves verification methods only for did:key DIDs with Ed25519 keys; other DID methods verify only if your document loader resolves them. Gateways must be plain origins, since the arbitrary gateway paths that FEP-ef61 discusses are not supported. The full list is in Not supported.
The profile was tested against tootik v0.25.4 and Mitra v5.10.0 in #1202. Fedify verifies tootik's portable actors, activities, and HTTP Signatures, and Mitra's gateway HTTP Signatures on activities that a client signed. Both accept the portable activities that Fedify sends. Two fixes came out of this testing. Fedify now reads a gateways list that tootik leaves unmapped in its JSON-LD context. It also serializes gateways as bare origins, because Mitra rejects a trailing slash. Fedify has not yet verified an actor or activity that Mitra signed itself, and Mastodon's handling of portable actors has not been tested. The FEP-ef61 interoperability section of FEDERATION.md records what was tested and how.
fedify lookup looks up portable objects by their ap: and ap+ef61: IDs, compatible identifiers, and the handles of portable actors, and it verifies their proofs. When no gateway returns an acceptable object, it says why. A new --gateway option on fedify lookup, fedify inbox, and fedify webfinger names the gateways to ask. fedify inbox --follow follows portable actors, and fedify webfinger accepts portable actor IDs. See Looking up portable objects in the CLI manual.
The mock federation and contexts in @fedify/testing support portable IDs, verification against fixture document loaders, gateway requests, and hashlink media, so tests can exercise portable objects without a live gateway. @fedify/lint's actor URI rules accept the portable IDs that the getPortable*Uri() methods build. @fedify/next now passes hashlink media requests to Fedify, a problem reported in #1149 and fixed in #1170; to have Next.js run the middleware for them, add { source: "/.well-known/apgateway/:path*" } to your matcher.
Fedify already runs activity delivery and inbox processing on queue workers, but applications usually have jobs of their own: digest emails, timeline rebuilds, link previews. Until now, those jobs could not use Fedify's queues, serialization, or retries. Fedify 2.4.0 opens the same machinery to application-defined tasks.
A task is defined once with defineTask() and enqueued from any Context with enqueueTask() or enqueueTaskMany(). Every task takes a Standard Schema, such as a Zod or Valibot schema, which types its payload. Fedify validates the payload when it is enqueued and again when a worker picks it up; the second check catches payloads from an earlier deployment that no longer match the schema:
import { createExponentialBackoffPolicy } from "@fedify/fedify";
import { z } from "zod";
const sendDigest = federation.defineTask("sendDigest", {
schema: z.object({ userId: z.string(), since: z.date() }),
handler: async (ctx, data) => {
// data is typed as { userId: string; since: Date }
await sendEmail(data.userId, await buildDigest(data.userId, data.since));
},
retryPolicy: createExponentialBackoffPolicy({ maxAttempts: 3 }),
});
// Anywhere with a Context:
await ctx.enqueueTask(sendDigest, { userId: "alice", since: new Date() }, {
delay: { minutes: 30 },
deduplicationKey: "digest:alice",
});
Fedify serializes payloads with devalue, so Date, Map, Set, URL, bigint, Temporal values, circular references, and Activity Vocabulary objects all survive the trip through any message queue backend. A Note in a payload comes back as a Note. Failed handlers are retried with exponential backoff by default, and queues with native retries handle retries themselves. Tasks fall back to the outbox queue unless you give them their own. Use the new task slot of the queue option or a per-task queue for that, and startQueue(ctxData, { queue: "task" }) runs a task-only worker. Setting taskQueueResolution: "strict" turns the fallback into an error.
A deduplicationKey deduplicates enqueues. The feature came from @julian's point in #206 that recurring tasks are hard to manage when you cannot tell whether one is already queued. A queue that declares nativeDeduplication enforces the key itself; otherwise, Fedify keeps a best-effort marker in the key–value store through cas(). Each task runs in a fedify.task span, and the queue metrics from 2.3.0 now include task queues. The Background tasks chapter covers routing, retries, deduplication, and the limitations; cron-style scheduling, result backends, and priorities are out of scope for now.
ChanHaeng Lee (@2chanhaeng) implemented the API: the core in #803, deduplication in #806, and observability in #812, following the plans in #797, #798, and #799. The work was merged in #923, closing #206, which had been open since February 2025.
Fedify 2.2.0 added client-to-server outbox listeners, but a C2S client also has to upload images and video before it can post them. That traffic does not go through POST /outbox. Fedify 2.4.0 supports the ActivityPub Media Upload extension, a SocialCG proposal last updated in 2017 that never reached W3C Recommendation status.
setMediaUploader() registers a multipart/form-data endpoint. Its callback receives the uploaded file and the object shell the client posted along with it. If the callback returns the created object, Fedify responds with 201 Created; if it returns a URL where the object will appear once processing finishes, Fedify responds with 202 Accepted:
federation
.setMediaUploader(
"/users/{identifier}/media",
async (ctx, identifier, file, object) => {
const stored = await uploadToStorage(file);
return new Image({
id: ctx.getObjectUri(Image, { uuid: stored.uuid }),
url: new URL(stored.publicUrl),
mediaType: file.type,
name: object.name,
});
},
)
.authorize(async (ctx, identifier) => {
const session = await verifyAccessToken(
ctx.request.headers.get("authorization"),
);
return session?.identifier === identifier;
});
The actor dispatcher advertises the endpoint itself by setting endpoints: new Endpoints({ uploadMedia: ctx.getMediaUploaderUri(identifier) }), the same way it places its inbox and outbox URIs. Without an authorize() hook, anyone who finds the endpoint can upload to it. For that reason, Fedify logs a warning when an uploader is registered without one, and when the returned object's URI does not match an object dispatcher or the actor does not advertise the endpoint. Four new @fedify/lint rules catch the same mistakes before they run. Fedify reads the upload into memory and sets no size limit of its own, so cap the request size at your reverse proxy. Storing the file and serving the object back are up to the application. See the Media upload chapter; the work is in #754 and #927.
Fedify decides several things about every inbox delivery: which signature mechanisms applied, which keys they tried, whether the actor owned the key, and what happened to the activity. Before this release, applications saw only part of that. A Linked Data Signature by a key the actor did not own was counted the same as a bad signature, and the key that verified a request was never handed to the application. The request in #1191 came from an effort to log these decisions for a debugging tool.
The new onRequestFinished() callback on inbox listeners receives an InboxRequestReport for every delivery to an inbox route, including rejected requests and requests that fail before verification:
federation
.setInboxListeners("/users/{identifier}/inbox", "/inbox")
.on(Follow, async (ctx, follow) => { /* ... */ })
.onRequestFinished(async (ctx, report) => {
await db.insertInboxLog({
inbox: report.inbox.kind, // "personal" | "shared" | "portable"
authentication: report.authentication.status,
outcome: report.outcome.type === "response"
? report.outcome.disposition // "processed" | "enqueued" | "duplicate" | ...
: `exception:${report.outcome.stage}`,
keyIds: report.attempts
.flatMap((attempt) => attempt.checks)
.flatMap((check) => check.triedKeys)
.map((key) => key.id?.href),
});
});
Each HTTP Signature, Linked Data Signature, or Object Integrity Proof check appears as an attempt with the actual keys it tried, including a stale cached key and its refreshed replacement. The report also carries the final authentication decision, such as actorKeyMismatch or proofPolicy, and the response or exception that ended the request. Fedify awaits the callback whether or not the trace is sampled. Errors it throws are logged without changing the response, and enqueued means the queue accepted the activity, not that a worker later processed it. Mock federations in @fedify/testing accept the same callback. See Observing inbox requests; the implementation is in #1201.
Actor dispatchers have been able to return a Tombstone since 2.2.0, but object dispatchers could only return null. A deleted post was answered with 404 Not Found, as if it had never existed, so applications that wanted 410 Gone, as ActivityPub recommends, needed their own route in front of Fedify.
Object dispatchers may now return a Tombstone, which Fedify serves with 410 Gone and the serialized tombstone after applying the authorization predicate:
federation.setObjectDispatcher(
Note,
"/users/{identifier}/notes/{id}",
async (ctx, values) => {
const post = await getPost(values.identifier, values.id);
if (post == null) return null;
if (post.deletedAt != null) {
return new Tombstone({
id: ctx.getObjectUri(Note, values),
formerType: Note,
deleted: post.deletedAt,
});
}
return new Note({ id: ctx.getObjectUri(Note, values), content: post.content });
},
);
The return type of ObjectDispatcher widens to TObject | Tombstone | null. RequestContext.getObject() still returns null for a tombstone unless you pass { tombstone: "passthrough" } or ask for the Tombstone or Object class itself. Dispatchers registered for Tombstone or Object that already returned tombstones now respond with 410 rather than 200. See Deleted objects; the change was proposed in #1112 and made in #1117.
Much of what Fedify fetches is chosen by the sender of an incoming request. Key IDs in signatures are one example, and the servers behind them can be slow on purpose. Several changes in 2.4.0 bound that work.
As #1130 points out, a request may carry several RFC 9421 signatures, and each one can make Fedify fetch a key from a URL of the sender's choosing. Fedify now verifies at most the first three, in Signature-Input order, and ignores the rest. A signature counts toward the limit even if it fails before its key is fetched, and a key named by several signatures is looked up once. The new FederationOptions.maxHttpSignatures option, added with the limit in #1166, changes it.
The built-in document loaders now give each call ten seconds by default, a limit proposed in #1131 and added in #1169. The time limit covers every redirect, alternate document, double-knocking retry, and the response body. A timed-out call throws a FetchError whose cause is a DOMException named "TimeoutError". Error responses are read in full before the loader throws, up to 1 MiB, so that a server cannot send a 404 header and then stall the body. FederationOptions.documentLoaderTimeout changes the limit or turns it off with null, and custom document loaders are not affected. Since #913, getDocumentLoader() also rejects HTML responses that do not advertise an ActivityPub alternate with a FetchError, rather than failing inside JSON.parse(), which #912 found cluttering error trackers.
Cached public keys and remembered HTTP Message Signatures specs used to live forever in the key–value store. #1017 reports a production Redis instance holding 194,424 public-key entries, about 165 MiB, for actors that had mostly stopped federating. Keys now expire after 30 days and specs after 90, configurable through FederationOptions.publicKeyTtl and FederationOptions.httpMessageSignaturesSpecTtl. Entries written by earlier versions have no expiry until they are next written. The Clearing legacy cache entries section shows how to remove them for Redis, PostgreSQL, and other stores. Heewon Chae (@heeoneie) implemented this in #1027, their first contribution to Fedify.
const federation = createFederation<void>({
kv,
maxHttpSignatures: 1, // default: 3
documentLoaderTimeout: { seconds: 5 }, // default: 10 seconds
publicKeyTtl: { days: 7 }, // default: 30 days
httpMessageSignaturesSpecTtl: { days: 30 }, // default: 90 days
});
Finally, verifyProof() now authenticates every option of an Ed25519 JCS proof except proofValue, including expires, domain, challenge, and nonce. It also rejects expired or malformed ones. Previously, a field such as expires could be added to an already signed proof without invalidating it. Jiwon Kwon (@z9mb1) spotted this in review of #968. Callers can now require a domain and challenge through VerifyProofOptions to prevent cross-domain or replayed use.
@fedify/adonisjs@fedify/adonisjs integrates Fedify with AdonisJS. Running node ace add @fedify/adonisjs registers a service provider that owns the federation's lifecycle and a server middleware that mounts it. It also scaffolds config/fedify.ts, start/federation.ts, and app/federation/main.ts. Controllers get a ctx.federation request context:
// app/federation/main.ts
import federation from "@fedify/adonisjs/services/builder";
import { Person } from "@fedify/vocab";
federation.setActorDispatcher("/actors/{identifier}", (ctx, identifier) =>
new Person({ id: ctx.getActorUri(identifier), preferredUsername: identifier }));
The middleware sits first in AdonisJS's server stack rather than its router stack. That way Fedify sees paths the router does not know, such as /.well-known/webfinger, and reads request bodies before the body parser consumes them. The package targets Node.js and is published to npm only. See the AdonisJS section of the integration guide.
Samuel Brinkmann (@sabrinkmann) contributed the package in #1006, closing #139, and built it on a prototype that Emelia Smith (@thisismissem) attached to that issue.
npm add @fedify/adonisjs
pnpm add @fedify/adonisjs
yarn add @fedify/adonisjs
@fedify/interaction-controlsGoToSocial interaction controls, FEP-044f quote authorization, and FEP-7aa9 featured collections share a request-and-authorization flow. A server asks to reply to, quote, or feature a post, and the author's server approves according to the post's interactionPolicy. @fedify/interaction-controls implements that flow for like, reply, announce, quote, and feature interactions. Each has a helper, such as likeInteraction or quoteInteraction, that creates and verifies requests and authorizations, evaluates policies, and recognizes interactions that arrived without a request:
import { likeInteraction } from "@fedify/interaction-controls";
// When a LikeRequest arrives:
const verified = await likeInteraction.verifyRequest(ctx, { request });
if (verified.verified) {
const decision = await likeInteraction.evaluatePolicy(ctx, {
subject: note,
requester: request.actorId!,
});
if (decision.result === "automatic") { // or "manual" or "denied"
const authorization = likeInteraction.createAuthorization({
id: new URL("https://example.com/authorizations/1"),
attributedTo: owner,
interactingObject: like,
interactionTarget: note,
});
// ...
}
}
The package does not install inbox listeners or store anything; the application decides where requests and authorizations live and when a human needs to approve. Verification fails closed. Without a policy, likes, replies, and announces are approved automatically, while quotes and features are denied. @fedify/vocab gains the FEP-7aa9 types this needs: FeaturedCollection, FeaturedItem, FeatureRequest, and FeatureAuthorization, along with actors' featuredCollections and InteractionPolicy.canFeature. Samuel Brinkmann asked for them in #810 once Mastodon 4.6 shipped the FEP. See the Interaction controls chapter; the work is in #914 and #929.
npm add @fedify/interaction-controls
pnpm add @fedify/interaction-controls
yarn add @fedify/interaction-controls
deno add jsr:@fedify/interaction-controls
bun add @fedify/interaction-controls
@fedify/netlify@fedify/netlify lets a Fedify application run on Netlify Functions without an external database or queue. NetlifyMessageQueue submits jobs as Netlify Async Workloads events, and createNetlifyQueueHandler() turns a Function into their consumer, with delayed delivery, native retries, and per-key FIFO ordering. NetlifyBlobsKvStore is a key–value store on Netlify Blobs with expiration, prefix listing, and compare-and-swap:
import {
createNetlifyQueueHandler,
NetlifyBlobsKvStore,
NetlifyMessageQueue,
} from "@fedify/netlify";
import { AsyncWorkloadsClient } from "@netlify/async-workloads";
import { getStore } from "@netlify/blobs";
export const kv = new NetlifyBlobsKvStore(
getStore({ name: "fedify", consistency: "strong" }),
);
export const queue = new NetlifyMessageQueue({
client: new AsyncWorkloadsClient(),
orderingKv: kv,
});
// netlify/functions/fedify-queue.ts
export default createNetlifyQueueHandler({
queue,
maxRetries: 4,
federation: () => builder.build({ kv, queue, manuallyStartQueue: true }),
});
The key–value store exists because of cost. #1010 describes a static blog whose ActivityPub traffic kept a Netlify Database from ever idling, which used most of the account's monthly credits. Jiwon Kwon contributed NetlifyBlobsKvStore in #1029, and the queue landed in #934. See Netlify Functions in the deployment guide.
npm add @fedify/netlify
pnpm add @fedify/netlify
yarn add @fedify/netlify
deno add jsr:@fedify/netlify
bun add @fedify/netlify
@fedify/pglite@fedify/pglite provides PgliteKvStore, a KvStore on an embedded PGlite database that you create and pass in. It uses the same schema as PostgresKvStore. It is meant for a single PGlite instance in a single runtime isolate, so it does not include a message queue: PGlite does not share data between processes.
import { PGlite } from "@electric-sql/pglite";
import { PgliteKvStore } from "@fedify/pglite";
const federation = createFederation({
kv: new PgliteKvStore(new PGlite("./data/fedify")),
});
ChanHaeng Lee contributed the package in #1020, together with testKvStore() in @fedify/testing. testKvStore() is a conformance suite for KvStore implementations, the counterpart to testMessageQueue(). See PgliteKvStore.
npm add @fedify/pglite
pnpm add @fedify/pglite
yarn add @fedify/pglite
deno add jsr:@fedify/pglite
bun add @fedify/pglite
Portable inbox forwarding, task deduplication, Netlify queue ordering, and the delivery circuit breaker all need an atomic compare-and-swap on the key–value store. PostgresKvStore now implements cas(), added in #934, and so does RedisKvStore, requested in #1163 and added in #1167. The Redis version uses a single-key Lua script, which works on standalone Redis and Redis Cluster. Before this, the circuit breaker fell back to racy updates on Redis.
Both come with conditions. RedisKvStore needs Redis to permit EVAL, and custom codecs must encode equal values identically. PostgresKvStore now creates logged tables by default, because losing sequence state in crash recovery could block queued events for good. It migrates existing unlogged tables when it initializes. That one-time migration rewrites the table and locks it exclusively, so schedule the upgrade accordingly if the table is large or busy; unlogged: true keeps the old behavior.
fedify init and the CLIfedify init can now scaffold a SvelteKit project, contributed by Jang Hanarae (@menele) in #971. Jang Hanarae also added a test task to generated projects in #990. It starts the app, waits for it to be ready, and checks that it resolves a local actor, which gives new projects a smoke test to run after scaffolding and after later changes. Before generating anything, fedify init now checks that the selected Deno, Bun, or Node.js meets Fedify's minimum version, or a framework's higher one, such as Astro's Node.js 22.12. It fails with a clear error in non-interactive mode, and in interactive mode it disables the affected package managers. Lee Jeongmin (@userjmmm) contributed the check in #981. Astro projects now use Astro 7, and since #936 @fedify/astro is tested against Astro 5, 6, and 7 with real builds on Node.js, Deno, and Bun, as #931 asked.
fedify tunnel gains fedify.com.es, a tunneling service run by the Fedify project that needs no account; the CLI pins its SSH host key and refuses a mismatched server before exposing a local port. localhost.run is removed in the same change, #940, because the service no longer exists. fedify lookup now times out each request after ten seconds when -T/--timeout is not given, and the option now also bounds the wait for a DNS lookup. The CLI moves to Optique 1.3.2 in #1197, which handles attached values like --timeout=30 consistently, gives better suggestions for mistyped options, and no longer hides errors for known options behind positional arguments before --.
@fedify/lint gains several rules beyond the media upload ones mentioned above. actor-preferred-username-required, requested in #895 and added in #1022, warns when an actor dispatcher returns an actor without preferredUsername. outbox-listener-delivery-not-awaited, from #1057 and #1067, reports an outbox listener that calls ctx.sendActivity() or ctx.forwardActivity() and drops the promise. On Cloudflare Workers, such an activity may never leave, because pending work is discarded once the response returns. await, return, Promise.all(), Promise.allSettled(), and waitUntil() count as handling the promise, while void, Promise.race(), and Promise.any() are accepted as a deliberate choice not to wait. The rule is a warning in the ESLint recommended configuration and an error in strict. Oxlint users enable it by name, and it is not available in Deno Lint.
In #1050, which resolves #900, the existing outbox-listener-delivery-required rule began following control flow instead of scanning the listener's source as text. It reports a listener whose only delivery calls sit in a dead branch, after an unconditional return or throw, or in a function that is never used, and it stays quiet when it cannot tell. Jae-Hyuk-Jang (@jhyuk) contributed this change and both new rules. archie (@ArchieTansaria) then taught both delivery rules about statically false loops and loop binding patterns in #1088. See the linting guide.
@fedify/vocab supports the FEP-22cd draft for translations. A Translation records a translated version's language, translators, source object, and source review time, and the new Object.translations property attaches them to a post without creating separate posts. See Translation metadata. (#1037, #1038)@fedify/vocab-tools gains an extraContext property option, which adds a JSON-LD context only when its terms are used, and a trustEmbeddedObjects type option. (#1037, #1038)suppressError: true handles are now logged as warnings rather than errors, so that they no longer show up as application errors. Contributed by Jae Hui Hong (@axz0612). (#933, #1035)enqueueMany() makes separate sends can set MessageQueue.atomicEnqueueMany to false. Fedify then rejects a multi-message batch governed by one deduplicationKey up front, since a partial send followed by a retry could enqueue duplicates. (#930, #934)Temporal bundle temporal-polyfill instead of requiring @js-temporal/polyfill at runtime, and type declarations rely on the standard esnext.temporal lib. (#823, #925)Accept header now includes the ActivityStreams profile on the JSON-LD media type, as ActivityPub and FEP-ef61 gateways require. (#834, #1077)fedify lookup --recurse reporting a timeout or other network failure as a possibly private object; it now reports the actual cause. (#1156, #1199)-p/--allow-private-address option of fedify webfinger being ignored. (#1156, #1199)outbox-listener-delivery-required and outbox-listener-delivery-not-awaited losing track of an object's functions when a nested helper assigns another property of that object. (#1125, #1140)npm add @fedify/fedify@2.4.0
pnpm add @fedify/fedify@2.4.0
yarn add @fedify/fedify@2.4.0
deno add jsr:@fedify/fedify@2.4.0
bun add @fedify/fedify@2.4.0
Update the other @fedify/* packages you use to 2.4.0 at the same time. To update the CLI, run npm install -g @fedify/cli@2.4.0, or see the installation instructions for other package managers.
Most applications need no code changes, but these changes in behavior may affect you:
401 Unauthorized unless they carry a valid Object Integrity Proof by the actor's DID. Keys at compatible identifiers, and keys at ordinary URLs that claim a portable owner, are no longer trusted by origin.Context as their options, such as await create.getObject(ctx), now verify the portable objects they dereference, including references to compatible identifiers from objects with ordinary HTTP(S) IDs./.well-known/apgateway/, and Context.sendActivity() may reject an activity that embeds others' portable objects. Next.js applications need the matcher shown above to serve hashlink media.maxHttpSignatures if you need more.fedify lookup does too.Tombstone or Object that return tombstones now respond with 410 Gone instead of 200 OK.PostgresKvStore migrates existing unlogged tables to logged tables on initialization, which locks the table.RedisKvStore needs EVAL permission for cas().Context and RequestContext interfaces need the new getPortable*Uri() methods and isSignedByAudience().fedify tunnel no longer supports localhost.run.Thanks to this release's contributors:
@fedify/adonisjs, and the request for FEP-7aa9 vocabularytest task in fedify initfedify initactor-preferred-username-required and outbox-listener-delivery-not-awaited lint rules, and control-flow analysis for outbox-listener-delivery-requiredChanHaeng Lee (@2chanhaeng) and Jiwon Kwon (@z9mb1) joined as maintainers after 2.3.0. ChanHaeng built the background task API and @fedify/pglite, Jiwon built NetlifyBlobsKvStore and rewrote the relay documentation, and both reviewed much of the rest of this release.
Many people added tests, documentation, and infrastructure during this cycle, often starting from a good first issue: Seoa Yoon (@bananamilk452), Lim Kyoujin (@dktsudgg), @m2nsp, Cho Seongmin (@seongmin36), Safal Shrestha (@Safal-Shrestha-SS), Jo Youngjae (@jaonz6057), @kecan0406, Loi Nguyen (@lntutor), Oh Taejun (@0w0), Yerin Park (@dstforye), Lee Dogeon (@moreal), and Sae Jin Kim (@siliconsjang).
The FEP-ef61 work owes a lot to people outside the project. @anotherdoesnm opened the request in #288. @silverpill, the author of FEP-ef61 and FEP-ae97, clarified the gateway trust model, reviewed the compound proof profile, and checked its test vectors. @dmitrizagidulin and @by_caballero weighed in on the profile's JSON-LD questions in #938. Testing against tootik and Mitra led to two of the interoperability fixes in this release.
See CHANGES.md for the complete changelog.
github.com
ActivityPub server framework in TypeScript. Contribute to fedify-dev/fedify development by creating an account on GitHub.
Fedify 2.4.0 is out.
The main addition is support for FEP-ef61 portable objects. A Fedify server can now host actors whose identity is a DID instead of a domain, serve their signed objects through /.well-known/apgateway/, and verify the portable objects of other servers. I tested it against tootik and Mitra, and wrote up where Fedify's profile differs from the FEP and what it leaves out.
The release also adds application-defined background tasks, the ActivityPub Media Upload extension, per-request inbox reports, 410 Gone for deleted objects, and four new packages (AdonisJS, Netlify, PGlite, and interaction controls).
Some defaults changed, so please read the upgrading section before you update, especially if you federate with software that uses FEP-ef61 compatible identifiers.
Release notes: https://hackers.pub/@fedify/2026/fedify-2-4.
Thanks to everyone who contributed to this cycle, and to @silverpill for patient answers about the FEP.
hackers.pub
Fedify is a TypeScript framework for building ActivityPub servers. It implements federation details such as HTTP Signatures, JSON-LD processing, WebFinger, inbox and outbox routing, and activity delivery.
Fedify 2.4.0 is a minor release with an unusually long changelog. The main addition is support for FEP-ef61 portable objects: a Fedify server can now host actors whose identity is a DID rather than a domain, serve and verify their signed objects, and consume the portable objects of other servers. The release also adds an API for application-defined background tasks, the ActivityPub Media Upload extension, a hook that reports how every inbox request was authenticated and handled, and 410 Gone tombstones for deleted objects. Several defaults change to limit what a hostile peer can make Fedify do. Four new packages ship with this version: @fedify/adonisjs, @fedify/interaction-controls, @fedify/netlify, and @fedify/pglite.
Most applications can upgrade without code changes, but a few of the new defaults are behavior changes. They are listed in the Upgrading section at the end.
An ActivityPub object's ID is an HTTP(S) URL on its server's domain. If that server shuts down, its IDs stop resolving, and an actor that moves elsewhere gets a new ID that everyone has to follow again. FEP-ef61, written by @silverpill of Mitra, proposes IDs that do not name a server at all. A portable ID is an ap: URI whose authority is a DID:
ap://did:key:z6MkrJVnaZkeFzdQyMZu1cgjg7k1pZZ6pvBQ7XJPt4swbTQ2/actor
The same object can be stored on several servers, called gateways, each of which serves it under /.well-known/apgateway/. Because no server owns the ID, a portable actor, activity, or object is authenticated only by its FEP-8b32 Object Integrity Proof, made with a key of the DID in its ID. A portable actor lists its gateways in a gateways property, and other servers deliver to its inbox through them.
Fedify 2.4.0 implements a profile of FEP-ef61 for gateways and consumers of portable objects. It makes two deliberate choices where the FEP or the specifications it rests on leave questions open, and it leaves out a few parts of the proposal; both are described below. The new Portable objects chapter of the manual walks through running portable actors from start to finish and then describes the profile as a whole, so that other implementers can tell what to expect from a Fedify server. The request came from @anotherdoesnm in #288, which tracked the work from a roadmap to the final pieces.
Portable objects are a foundation for nomadic identity, not nomadic identity itself. Fedify serves and verifies portable objects and forwards activities between gateways, but it does not copy actors or posts between servers, rotate keys, or move an actor to another DID. Full nomadic identity is still open in #413.
A portable actor starts with an Ed25519 key pair. Its DID is made from the public key with exportDidKey(), and whoever holds the private key can act as the actor, so store it with the actor and keep it safe:
import { generateCryptoKeyPair } from "@fedify/fedify";
import { exportDidKey } from "@fedify/vocab-runtime";
const { publicKey, privateKey } = await generateCryptoKeyPair("Ed25519");
const did = await exportDidKey(publicKey); // did:key:z6Mk...
// The verification method that the actor's proofs refer to:
const didKeyId = new URL(`${did}#${did.slice("did:key:".length)}`);
The actor dispatcher returns a portable actor as it would an ordinary one. Context.getPortableActorUri() and its siblings for inboxes, outboxes, collections, and objects build portable IDs from the dispatcher's paths and the DID, and signObject() signs the actor with the DID's key:
federation
.setActorDispatcher("/users/{identifier}", async (ctx, identifier) => {
const user = await findUser(identifier);
if (user == null) return null;
// A request through the gateway endpoint names the DID in its path,
// e.g., GET /.well-known/apgateway/did:key:z6Mk.../users/alice:
if (
ctx.portableRequest != null &&
ctx.portableRequest.authority !== user.did
) {
return null;
}
// This server's gateway keys for the actor (see below):
const keys = await ctx.getActorKeyPairs(identifier);
return await signObject(
new Person({
id: ctx.getPortableActorUri(identifier, user.did), // ap+ef61://did:key:z6Mk.../users/alice
preferredUsername: user.username,
inbox: ctx.getPortableInboxUri(identifier, user.did),
outbox: ctx.getPortableOutboxUri(identifier, user.did),
followers: ctx.getPortableFollowersUri(identifier, user.did),
gateways: [new URL("https://example.com")],
publicKeys: keys.map((key) => key.cryptographicKey),
assertionMethods: keys.map((key) => key.multikey),
}),
user.didKeyPair.privateKey,
new URL(`${user.did}#${user.did.slice("did:key:".length)}`),
);
})
.setKeyPairsDispatcher(async (ctx, identifier) => {
const user = await findUser(identifier);
// This server's own key pair for the actor, not the DID's:
return user == null ? [] : [user.gatewayKeyPair];
})
.mapPortableActorId(async (ctx, identifier) => {
const user = await findUser(identifier);
return user == null ? null : ctx.getPortableActorUri(identifier, user.did);
});
With this dispatcher, Fedify serves the actor at its compatible identifier, https://example.com/.well-known/apgateway/did:key:z6Mk.../users/alice, which is how other servers retrieve it. It serves it through WebFinger as @alice@example.com, since the handle's domain comes from the first gateway in gateways, and at its ordinary URL as before. On the gateway endpoint, Fedify serves a document only if its ID is the requested portable ID and it carries a proof made with a key of that DID.
The key pairs dispatcher and the new ActorCallbackSetters.mapPortableActorId() give the actor gateway keys. These are the server's own keys for the actor, and they sign only the HTTP requests the server makes on the actor's behalf, such as deliveries. They never sign the actor's activities or objects; only the DID's key does that. The actor document lists them under assertionMethods, as FEP-521a describes, so that other servers can verify the HTTP Signatures. See Dispatching the actor and Gateway keys of portable actors for the details.
Object and collection dispatchers serve portable objects and collections through the gateway endpoint in the same way, routed by the path after the DID. Two rules protect the actor's data. A non-public portable object is served only if the dispatcher has an authorization predicate, because FEP-ef61 forbids a gateway to show it to anyone outside its audience; the new RequestContext.isSignedByAudience() helps to write one. Without a predicate, a request for such an object gets 404 Not Found, as though the gateway did not store it. A deleted object is served as a signed Tombstone with 410 Gone. Resources that portable objects refer to by SHA-256 hashlinks, such as GET /.well-known/apgateway/hl:zQm..., are served by the new Federatable.setHashlinkMediaDispatcher().
A portable actor's activities have to be signed by its DID, which Fedify's key pairs dispatcher does not know about. Sign them with signObject() before sending, and pass normalizeExistingProofs: true so that the activity goes out in the form its proof covers. An activity that embeds a portable object, such as the Note of a Create, is signed inside out:
const actor = withGatewayHints(
ctx.getPortableActorUri(user.identifier, user.did),
[new URL("https://example.com")],
); // ap+ef61://did:key:z6Mk.../users/alice?@gateway=https%3A%2F%2Fexample.com
const note = await signObject(
new Note({
id: ctx.getPortableObjectUri(
Note,
{ identifier: user.identifier, id: crypto.randomUUID() },
user.did,
),
attribution: actor,
to: PUBLIC_COLLECTION,
content: "Hello, world!",
}),
user.didKeyPair.privateKey,
didKeyId,
);
const create = await signObject(
new Create({
id: ctx.getPortableObjectUri(
Note,
{ identifier: user.identifier, id: crypto.randomUUID() },
user.did,
),
actor,
to: PUBLIC_COLLECTION,
object: note,
}),
user.didKeyPair.privateKey,
didKeyId,
);
await ctx.sendActivity(
{ identifier: user.identifier },
"followers",
create,
{ normalizeExistingProofs: true },
);
This example depends on a fix to signObject() that is useful outside FEP-ef61 as well. Previously, a signed object assigned to a typed parent was rebuilt under the parent's JSON-LD context when the parent was serialized. The child kept its proofValue but lost the document its proof covered, so it no longer verified on its own. signObject() now captures the exact document it signed, and nested serialization embeds that document verbatim. The captured document is a snapshot: clone() does not carry it, so sign a clone again if it has to be embedded. The design is in #1044 and the fix in #1051.
Context.sendActivity() refuses a portable activity that is unsigned, signed by the wrong key, or carries a Linked Data Signature, and throws a TypeError before anything is queued. It also refuses a few shapes of compound document that Fedify's own inboxes would reject, such as a received signed Follow embedded in an Accept; refer to such an object by its ID instead. Deliveries to portable inboxes go through the recipient's gateways, or else the @gateway hints on the inbox, trying at most five of them until one accepts. Before this release, they failed with UrlError: Unsupported protocol: ap+ef61:. The rules are in Delivering to portable actors and Producing a compound document.
On the receiving side, a portable inbox is reached as POST /.well-known/apgateway/did:key:z6Mk.../users/alice/inbox and dispatched to the same inbox listeners as the ordinary inbox. Context.parseUri() gains a portable option, and the parsed result has the DID in a new authority property to check against the one you store. Fedify accepts an activity of a portable actor, or one with a portable ID, only if it has a valid proof by that DID; neither an HTTP Signature nor a Linked Data Signature authenticates it. Each portable object embedded in the activity is verified against its own proof before dispatch. An accepted activity that carries its own proof is forwarded to the actor's other gateways at most once, as FEP-ef61 recommends. Forwarding needs a key–value store that supports cas(), and the FederationOptions.portableInboxForwarding option tunes it. See Portable inboxes and Forwarding to other gateways.
Applications that never host a portable actor still meet portable objects from other servers. Context.lookupObject() resolves portable IDs, compatible identifiers, and the fediverse handles of portable actors, and returns portable objects only if they verify:
const actor = await ctx.lookupObject("@alice@example.com");
// A portable ID without @gateway hints needs gateways to ask:
const note = await ctx.lookupObject("ap://did:key:z6Mk.../notes/1", {
gateways: ["https://example.com"],
});
Property accessors fetch portable references through gateways as well, and they verify what they fetch with the new verifyPortableObject option. Context has a matching verifyPortableObject property, so passing a context as the options, as in await create.getObject(ctx), is enough. Activities received in inboxes use this verifier by default, and objects fetched through their accessors inherit it, so a chained call such as (await create.getObject(ctx))?.getAttribution() verifies each portable object along the way. A lookup asks at most five gateways, falling back to the next when one fails or serves something that does not verify. Unsigned collections are accepted only from a gateway that the owner lists, and only for an actor's inbox, outbox, followers, following, and liked, as FEP-ef61 allows. fetchPortableMedia() retrieves the media of a portable object and returns it only if it matches the object's digestMultibase. See Dereferencing portable references and Default verifiers.
Software that does not understand ap: URIs can refer to a portable object by a compatible identifier, an HTTP(S) URL under a gateway's /.well-known/apgateway/ path such as https://gw.example/.well-known/apgateway/did:key:z6Mk.../actor. Some implementations, tootik among them, use compatible identifiers as the IDs of their portable actors.
Earlier versions of Fedify treated these URLs as ordinary web URLs and trusted them by the server that served them. Since anyone can serve a compatible identifier for any DID, https://evil.example/.well-known/apgateway/did:key:z6MkAlice/actor could act as Alice without a proof by Alice's DID. Fedify 2.4.0 treats a compatible identifier as the portable object it stands for, as FEP-ef61 requires. Inboxes reject an activity from an actor whose ID is a compatible identifier with 401 Unauthorized unless it has a valid Object Integrity Proof by the actor's DID, and there is no option to turn this off. Actors that publish compatible identifiers without signing them are no longer accepted, and neither are those whose DIDs use key types that Fedify cannot verify yet. The change was tracked in #1093 and made in #1105.
Your own portable actors may still use compatible identifiers as their IDs, for software that rejects ap: IDs. Build them with toCompatibleEf61Id() before signing, and Fedify will treat them as portable wherever it serves or sends them. It also warns if one is malformed or does not point at the actor's first gateway. See Compatible identifiers as actor IDs.
Two choices in Fedify's profile differ from the current FEP text or rest on semantics that are not settled yet. Fedify keeps both for the 2.x series, and either may change in Fedify 3.0.
The first is the scheme. FEP-ef61 recommends ap: and canonicalizes ap+ef61: to it, while also warning that the recommendation may change to ap+ef61:, since these IDs are meant only for portable objects. Fedify accepts both, compares them as equal, and serializes ap+ef61:. Parsing a document normalizes its portable IRIs to ap+ef61:, so re-serializing an object that another implementation signed with ap: IDs breaks its proof. Keep the received JSON of such documents, and compare portable IDs with arePortableUrisEqual() rather than as strings. The discussion is in #826 and #828, and the reasoning is in Canonical ap+ef61: scheme.
The second concerns compound documents. When a signed Note sits inside a signed Create, the verifier has to know which part of the JSON each proof covers, and neither FEP-8b32 nor Verifiable Credential Data Integrity defines that yet; the question is open in w3c/vc-data-integrity#350. Fedify uses an interim map-local profile, worked out in #938. Each JSON map with a proof is its own secured document, each embedded portable object needs its own proof and its own @context, and proof sets and chains are not supported. The profile authenticates the exact JSON values each proof covers; it does not promise that a child's JSON-LD expansion inside its parent matches its expansion on its own. @silverpill, @dmitrizagidulin, and @by_caballero reviewed the proposal in that thread, and @silverpill reproduced its test vectors independently. See Map-local compound proofs.
Fedify does not implement FEP-ae97, the companion proposal for client-side signing. There are no endpoints for registering actors, submitting client-signed activities to a portable outbox, or gateway discovery, and no media upload and deletion endpoints under /.well-known/apgateway-media. Your own outbox listeners can still relay activities that a client signed, unchanged. Fedify also does not copy or reconcile actors, objects, or collections between gateways, so running an actor on several gateways means keeping their data in sync yourself. There is no workflow for rotating a DID's key or migrating an actor to another DID. Fedify resolves verification methods only for did:key DIDs with Ed25519 keys; other DID methods verify only if your document loader resolves them. Gateways must be plain origins, since the arbitrary gateway paths that FEP-ef61 discusses are not supported. The full list is in Not supported.
The profile was tested against tootik v0.25.4 and Mitra v5.10.0 in #1202. Fedify verifies tootik's portable actors, activities, and HTTP Signatures, and Mitra's gateway HTTP Signatures on activities that a client signed. Both accept the portable activities that Fedify sends. Two fixes came out of this testing. Fedify now reads a gateways list that tootik leaves unmapped in its JSON-LD context. It also serializes gateways as bare origins, because Mitra rejects a trailing slash. Fedify has not yet verified an actor or activity that Mitra signed itself, and Mastodon's handling of portable actors has not been tested. The FEP-ef61 interoperability section of FEDERATION.md records what was tested and how.
fedify lookup looks up portable objects by their ap: and ap+ef61: IDs, compatible identifiers, and the handles of portable actors, and it verifies their proofs. When no gateway returns an acceptable object, it says why. A new --gateway option on fedify lookup, fedify inbox, and fedify webfinger names the gateways to ask. fedify inbox --follow follows portable actors, and fedify webfinger accepts portable actor IDs. See Looking up portable objects in the CLI manual.
The mock federation and contexts in @fedify/testing support portable IDs, verification against fixture document loaders, gateway requests, and hashlink media, so tests can exercise portable objects without a live gateway. @fedify/lint's actor URI rules accept the portable IDs that the getPortable*Uri() methods build. @fedify/next now passes hashlink media requests to Fedify, a problem reported in #1149 and fixed in #1170; to have Next.js run the middleware for them, add { source: "/.well-known/apgateway/:path*" } to your matcher.
Fedify already runs activity delivery and inbox processing on queue workers, but applications usually have jobs of their own: digest emails, timeline rebuilds, link previews. Until now, those jobs could not use Fedify's queues, serialization, or retries. Fedify 2.4.0 opens the same machinery to application-defined tasks.
A task is defined once with defineTask() and enqueued from any Context with enqueueTask() or enqueueTaskMany(). Every task takes a Standard Schema, such as a Zod or Valibot schema, which types its payload. Fedify validates the payload when it is enqueued and again when a worker picks it up; the second check catches payloads from an earlier deployment that no longer match the schema:
import { createExponentialBackoffPolicy } from "@fedify/fedify";
import { z } from "zod";
const sendDigest = federation.defineTask("sendDigest", {
schema: z.object({ userId: z.string(), since: z.date() }),
handler: async (ctx, data) => {
// data is typed as { userId: string; since: Date }
await sendEmail(data.userId, await buildDigest(data.userId, data.since));
},
retryPolicy: createExponentialBackoffPolicy({ maxAttempts: 3 }),
});
// Anywhere with a Context:
await ctx.enqueueTask(sendDigest, { userId: "alice", since: new Date() }, {
delay: { minutes: 30 },
deduplicationKey: "digest:alice",
});
Fedify serializes payloads with devalue, so Date, Map, Set, URL, bigint, Temporal values, circular references, and Activity Vocabulary objects all survive the trip through any message queue backend. A Note in a payload comes back as a Note. Failed handlers are retried with exponential backoff by default, and queues with native retries handle retries themselves. Tasks fall back to the outbox queue unless you give them their own. Use the new task slot of the queue option or a per-task queue for that, and startQueue(ctxData, { queue: "task" }) runs a task-only worker. Setting taskQueueResolution: "strict" turns the fallback into an error.
A deduplicationKey deduplicates enqueues. The feature came from @julian's point in #206 that recurring tasks are hard to manage when you cannot tell whether one is already queued. A queue that declares nativeDeduplication enforces the key itself; otherwise, Fedify keeps a best-effort marker in the key–value store through cas(). Each task runs in a fedify.task span, and the queue metrics from 2.3.0 now include task queues. The Background tasks chapter covers routing, retries, deduplication, and the limitations; cron-style scheduling, result backends, and priorities are out of scope for now.
ChanHaeng Lee (@2chanhaeng) implemented the API: the core in #803, deduplication in #806, and observability in #812, following the plans in #797, #798, and #799. The work was merged in #923, closing #206, which had been open since February 2025.
Fedify 2.2.0 added client-to-server outbox listeners, but a C2S client also has to upload images and video before it can post them. That traffic does not go through POST /outbox. Fedify 2.4.0 supports the ActivityPub Media Upload extension, a SocialCG proposal last updated in 2017 that never reached W3C Recommendation status.
setMediaUploader() registers a multipart/form-data endpoint. Its callback receives the uploaded file and the object shell the client posted along with it. If the callback returns the created object, Fedify responds with 201 Created; if it returns a URL where the object will appear once processing finishes, Fedify responds with 202 Accepted:
federation
.setMediaUploader(
"/users/{identifier}/media",
async (ctx, identifier, file, object) => {
const stored = await uploadToStorage(file);
return new Image({
id: ctx.getObjectUri(Image, { uuid: stored.uuid }),
url: new URL(stored.publicUrl),
mediaType: file.type,
name: object.name,
});
},
)
.authorize(async (ctx, identifier) => {
const session = await verifyAccessToken(
ctx.request.headers.get("authorization"),
);
return session?.identifier === identifier;
});
The actor dispatcher advertises the endpoint itself by setting endpoints: new Endpoints({ uploadMedia: ctx.getMediaUploaderUri(identifier) }), the same way it places its inbox and outbox URIs. Without an authorize() hook, anyone who finds the endpoint can upload to it. For that reason, Fedify logs a warning when an uploader is registered without one, and when the returned object's URI does not match an object dispatcher or the actor does not advertise the endpoint. Four new @fedify/lint rules catch the same mistakes before they run. Fedify reads the upload into memory and sets no size limit of its own, so cap the request size at your reverse proxy. Storing the file and serving the object back are up to the application. See the Media upload chapter; the work is in #754 and #927.
Fedify decides several things about every inbox delivery: which signature mechanisms applied, which keys they tried, whether the actor owned the key, and what happened to the activity. Before this release, applications saw only part of that. A Linked Data Signature by a key the actor did not own was counted the same as a bad signature, and the key that verified a request was never handed to the application. The request in #1191 came from an effort to log these decisions for a debugging tool.
The new onRequestFinished() callback on inbox listeners receives an InboxRequestReport for every delivery to an inbox route, including rejected requests and requests that fail before verification:
federation
.setInboxListeners("/users/{identifier}/inbox", "/inbox")
.on(Follow, async (ctx, follow) => { /* ... */ })
.onRequestFinished(async (ctx, report) => {
await db.insertInboxLog({
inbox: report.inbox.kind, // "personal" | "shared" | "portable"
authentication: report.authentication.status,
outcome: report.outcome.type === "response"
? report.outcome.disposition // "processed" | "enqueued" | "duplicate" | ...
: `exception:${report.outcome.stage}`,
keyIds: report.attempts
.flatMap((attempt) => attempt.checks)
.flatMap((check) => check.triedKeys)
.map((key) => key.id?.href),
});
});
Each HTTP Signature, Linked Data Signature, or Object Integrity Proof check appears as an attempt with the actual keys it tried, including a stale cached key and its refreshed replacement. The report also carries the final authentication decision, such as actorKeyMismatch or proofPolicy, and the response or exception that ended the request. Fedify awaits the callback whether or not the trace is sampled. Errors it throws are logged without changing the response, and enqueued means the queue accepted the activity, not that a worker later processed it. Mock federations in @fedify/testing accept the same callback. See Observing inbox requests; the implementation is in #1201.
Actor dispatchers have been able to return a Tombstone since 2.2.0, but object dispatchers could only return null. A deleted post was answered with 404 Not Found, as if it had never existed, so applications that wanted 410 Gone, as ActivityPub recommends, needed their own route in front of Fedify.
Object dispatchers may now return a Tombstone, which Fedify serves with 410 Gone and the serialized tombstone after applying the authorization predicate:
federation.setObjectDispatcher(
Note,
"/users/{identifier}/notes/{id}",
async (ctx, values) => {
const post = await getPost(values.identifier, values.id);
if (post == null) return null;
if (post.deletedAt != null) {
return new Tombstone({
id: ctx.getObjectUri(Note, values),
formerType: Note,
deleted: post.deletedAt,
});
}
return new Note({ id: ctx.getObjectUri(Note, values), content: post.content });
},
);
The return type of ObjectDispatcher widens to TObject | Tombstone | null. RequestContext.getObject() still returns null for a tombstone unless you pass { tombstone: "passthrough" } or ask for the Tombstone or Object class itself. Dispatchers registered for Tombstone or Object that already returned tombstones now respond with 410 rather than 200. See Deleted objects; the change was proposed in #1112 and made in #1117.
Much of what Fedify fetches is chosen by the sender of an incoming request. Key IDs in signatures are one example, and the servers behind them can be slow on purpose. Several changes in 2.4.0 bound that work.
As #1130 points out, a request may carry several RFC 9421 signatures, and each one can make Fedify fetch a key from a URL of the sender's choosing. Fedify now verifies at most the first three, in Signature-Input order, and ignores the rest. A signature counts toward the limit even if it fails before its key is fetched, and a key named by several signatures is looked up once. The new FederationOptions.maxHttpSignatures option, added with the limit in #1166, changes it.
The built-in document loaders now give each call ten seconds by default, a limit proposed in #1131 and added in #1169. The time limit covers every redirect, alternate document, double-knocking retry, and the response body. A timed-out call throws a FetchError whose cause is a DOMException named "TimeoutError". Error responses are read in full before the loader throws, up to 1 MiB, so that a server cannot send a 404 header and then stall the body. FederationOptions.documentLoaderTimeout changes the limit or turns it off with null, and custom document loaders are not affected. Since #913, getDocumentLoader() also rejects HTML responses that do not advertise an ActivityPub alternate with a FetchError, rather than failing inside JSON.parse(), which #912 found cluttering error trackers.
Cached public keys and remembered HTTP Message Signatures specs used to live forever in the key–value store. #1017 reports a production Redis instance holding 194,424 public-key entries, about 165 MiB, for actors that had mostly stopped federating. Keys now expire after 30 days and specs after 90, configurable through FederationOptions.publicKeyTtl and FederationOptions.httpMessageSignaturesSpecTtl. Entries written by earlier versions have no expiry until they are next written. The Clearing legacy cache entries section shows how to remove them for Redis, PostgreSQL, and other stores. Heewon Chae (@heeoneie) implemented this in #1027, their first contribution to Fedify.
const federation = createFederation<void>({
kv,
maxHttpSignatures: 1, // default: 3
documentLoaderTimeout: { seconds: 5 }, // default: 10 seconds
publicKeyTtl: { days: 7 }, // default: 30 days
httpMessageSignaturesSpecTtl: { days: 30 }, // default: 90 days
});
Finally, verifyProof() now authenticates every option of an Ed25519 JCS proof except proofValue, including expires, domain, challenge, and nonce. It also rejects expired or malformed ones. Previously, a field such as expires could be added to an already signed proof without invalidating it. Jiwon Kwon (@z9mb1) spotted this in review of #968. Callers can now require a domain and challenge through VerifyProofOptions to prevent cross-domain or replayed use.
@fedify/adonisjs@fedify/adonisjs integrates Fedify with AdonisJS. Running node ace add @fedify/adonisjs registers a service provider that owns the federation's lifecycle and a server middleware that mounts it. It also scaffolds config/fedify.ts, start/federation.ts, and app/federation/main.ts. Controllers get a ctx.federation request context:
// app/federation/main.ts
import federation from "@fedify/adonisjs/services/builder";
import { Person } from "@fedify/vocab";
federation.setActorDispatcher("/actors/{identifier}", (ctx, identifier) =>
new Person({ id: ctx.getActorUri(identifier), preferredUsername: identifier }));
The middleware sits first in AdonisJS's server stack rather than its router stack. That way Fedify sees paths the router does not know, such as /.well-known/webfinger, and reads request bodies before the body parser consumes them. The package targets Node.js and is published to npm only. See the AdonisJS section of the integration guide.
Samuel Brinkmann (@sabrinkmann) contributed the package in #1006, closing #139, and built it on a prototype that Emelia Smith (@thisismissem) attached to that issue.
npm add @fedify/adonisjs
pnpm add @fedify/adonisjs
yarn add @fedify/adonisjs
@fedify/interaction-controlsGoToSocial interaction controls, FEP-044f quote authorization, and FEP-7aa9 featured collections share a request-and-authorization flow. A server asks to reply to, quote, or feature a post, and the author's server approves according to the post's interactionPolicy. @fedify/interaction-controls implements that flow for like, reply, announce, quote, and feature interactions. Each has a helper, such as likeInteraction or quoteInteraction, that creates and verifies requests and authorizations, evaluates policies, and recognizes interactions that arrived without a request:
import { likeInteraction } from "@fedify/interaction-controls";
// When a LikeRequest arrives:
const verified = await likeInteraction.verifyRequest(ctx, { request });
if (verified.verified) {
const decision = await likeInteraction.evaluatePolicy(ctx, {
subject: note,
requester: request.actorId!,
});
if (decision.result === "automatic") { // or "manual" or "denied"
const authorization = likeInteraction.createAuthorization({
id: new URL("https://example.com/authorizations/1"),
attributedTo: owner,
interactingObject: like,
interactionTarget: note,
});
// ...
}
}
The package does not install inbox listeners or store anything; the application decides where requests and authorizations live and when a human needs to approve. Verification fails closed. Without a policy, likes, replies, and announces are approved automatically, while quotes and features are denied. @fedify/vocab gains the FEP-7aa9 types this needs: FeaturedCollection, FeaturedItem, FeatureRequest, and FeatureAuthorization, along with actors' featuredCollections and InteractionPolicy.canFeature. Samuel Brinkmann asked for them in #810 once Mastodon 4.6 shipped the FEP. See the Interaction controls chapter; the work is in #914 and #929.
npm add @fedify/interaction-controls
pnpm add @fedify/interaction-controls
yarn add @fedify/interaction-controls
deno add jsr:@fedify/interaction-controls
bun add @fedify/interaction-controls
@fedify/netlify@fedify/netlify lets a Fedify application run on Netlify Functions without an external database or queue. NetlifyMessageQueue submits jobs as Netlify Async Workloads events, and createNetlifyQueueHandler() turns a Function into their consumer, with delayed delivery, native retries, and per-key FIFO ordering. NetlifyBlobsKvStore is a key–value store on Netlify Blobs with expiration, prefix listing, and compare-and-swap:
import {
createNetlifyQueueHandler,
NetlifyBlobsKvStore,
NetlifyMessageQueue,
} from "@fedify/netlify";
import { AsyncWorkloadsClient } from "@netlify/async-workloads";
import { getStore } from "@netlify/blobs";
export const kv = new NetlifyBlobsKvStore(
getStore({ name: "fedify", consistency: "strong" }),
);
export const queue = new NetlifyMessageQueue({
client: new AsyncWorkloadsClient(),
orderingKv: kv,
});
// netlify/functions/fedify-queue.ts
export default createNetlifyQueueHandler({
queue,
maxRetries: 4,
federation: () => builder.build({ kv, queue, manuallyStartQueue: true }),
});
The key–value store exists because of cost. #1010 describes a static blog whose ActivityPub traffic kept a Netlify Database from ever idling, which used most of the account's monthly credits. Jiwon Kwon contributed NetlifyBlobsKvStore in #1029, and the queue landed in #934. See Netlify Functions in the deployment guide.
npm add @fedify/netlify
pnpm add @fedify/netlify
yarn add @fedify/netlify
deno add jsr:@fedify/netlify
bun add @fedify/netlify
@fedify/pglite@fedify/pglite provides PgliteKvStore, a KvStore on an embedded PGlite database that you create and pass in. It uses the same schema as PostgresKvStore. It is meant for a single PGlite instance in a single runtime isolate, so it does not include a message queue: PGlite does not share data between processes.
import { PGlite } from "@electric-sql/pglite";
import { PgliteKvStore } from "@fedify/pglite";
const federation = createFederation({
kv: new PgliteKvStore(new PGlite("./data/fedify")),
});
ChanHaeng Lee contributed the package in #1020, together with testKvStore() in @fedify/testing. testKvStore() is a conformance suite for KvStore implementations, the counterpart to testMessageQueue(). See PgliteKvStore.
npm add @fedify/pglite
pnpm add @fedify/pglite
yarn add @fedify/pglite
deno add jsr:@fedify/pglite
bun add @fedify/pglite
Portable inbox forwarding, task deduplication, Netlify queue ordering, and the delivery circuit breaker all need an atomic compare-and-swap on the key–value store. PostgresKvStore now implements cas(), added in #934, and so does RedisKvStore, requested in #1163 and added in #1167. The Redis version uses a single-key Lua script, which works on standalone Redis and Redis Cluster. Before this, the circuit breaker fell back to racy updates on Redis.
Both come with conditions. RedisKvStore needs Redis to permit EVAL, and custom codecs must encode equal values identically. PostgresKvStore now creates logged tables by default, because losing sequence state in crash recovery could block queued events for good. It migrates existing unlogged tables when it initializes. That one-time migration rewrites the table and locks it exclusively, so schedule the upgrade accordingly if the table is large or busy; unlogged: true keeps the old behavior.
fedify init and the CLIfedify init can now scaffold a SvelteKit project, contributed by Jang Hanarae (@menele) in #971. Jang Hanarae also added a test task to generated projects in #990. It starts the app, waits for it to be ready, and checks that it resolves a local actor, which gives new projects a smoke test to run after scaffolding and after later changes. Before generating anything, fedify init now checks that the selected Deno, Bun, or Node.js meets Fedify's minimum version, or a framework's higher one, such as Astro's Node.js 22.12. It fails with a clear error in non-interactive mode, and in interactive mode it disables the affected package managers. Lee Jeongmin (@userjmmm) contributed the check in #981. Astro projects now use Astro 7, and since #936 @fedify/astro is tested against Astro 5, 6, and 7 with real builds on Node.js, Deno, and Bun, as #931 asked.
fedify tunnel gains fedify.com.es, a tunneling service run by the Fedify project that needs no account; the CLI pins its SSH host key and refuses a mismatched server before exposing a local port. localhost.run is removed in the same change, #940, because the service no longer exists. fedify lookup now times out each request after ten seconds when -T/--timeout is not given, and the option now also bounds the wait for a DNS lookup. The CLI moves to Optique 1.3.2 in #1197, which handles attached values like --timeout=30 consistently, gives better suggestions for mistyped options, and no longer hides errors for known options behind positional arguments before --.
@fedify/lint gains several rules beyond the media upload ones mentioned above. actor-preferred-username-required, requested in #895 and added in #1022, warns when an actor dispatcher returns an actor without preferredUsername. outbox-listener-delivery-not-awaited, from #1057 and #1067, reports an outbox listener that calls ctx.sendActivity() or ctx.forwardActivity() and drops the promise. On Cloudflare Workers, such an activity may never leave, because pending work is discarded once the response returns. await, return, Promise.all(), Promise.allSettled(), and waitUntil() count as handling the promise, while void, Promise.race(), and Promise.any() are accepted as a deliberate choice not to wait. The rule is a warning in the ESLint recommended configuration and an error in strict. Oxlint users enable it by name, and it is not available in Deno Lint.
In #1050, which resolves #900, the existing outbox-listener-delivery-required rule began following control flow instead of scanning the listener's source as text. It reports a listener whose only delivery calls sit in a dead branch, after an unconditional return or throw, or in a function that is never used, and it stays quiet when it cannot tell. Jae-Hyuk-Jang (@jhyuk) contributed this change and both new rules. archie (@ArchieTansaria) then taught both delivery rules about statically false loops and loop binding patterns in #1088. See the linting guide.
@fedify/vocab supports the FEP-22cd draft for translations. A Translation records a translated version's language, translators, source object, and source review time, and the new Object.translations property attaches them to a post without creating separate posts. See Translation metadata. (#1037, #1038)@fedify/vocab-tools gains an extraContext property option, which adds a JSON-LD context only when its terms are used, and a trustEmbeddedObjects type option. (#1037, #1038)suppressError: true handles are now logged as warnings rather than errors, so that they no longer show up as application errors. Contributed by Jae Hui Hong (@axz0612). (#933, #1035)enqueueMany() makes separate sends can set MessageQueue.atomicEnqueueMany to false. Fedify then rejects a multi-message batch governed by one deduplicationKey up front, since a partial send followed by a retry could enqueue duplicates. (#930, #934)Temporal bundle temporal-polyfill instead of requiring @js-temporal/polyfill at runtime, and type declarations rely on the standard esnext.temporal lib. (#823, #925)Accept header now includes the ActivityStreams profile on the JSON-LD media type, as ActivityPub and FEP-ef61 gateways require. (#834, #1077)fedify lookup --recurse reporting a timeout or other network failure as a possibly private object; it now reports the actual cause. (#1156, #1199)-p/--allow-private-address option of fedify webfinger being ignored. (#1156, #1199)outbox-listener-delivery-required and outbox-listener-delivery-not-awaited losing track of an object's functions when a nested helper assigns another property of that object. (#1125, #1140)npm add @fedify/fedify@2.4.0
pnpm add @fedify/fedify@2.4.0
yarn add @fedify/fedify@2.4.0
deno add jsr:@fedify/fedify@2.4.0
bun add @fedify/fedify@2.4.0
Update the other @fedify/* packages you use to 2.4.0 at the same time. To update the CLI, run npm install -g @fedify/cli@2.4.0, or see the installation instructions for other package managers.
Most applications need no code changes, but these changes in behavior may affect you:
401 Unauthorized unless they carry a valid Object Integrity Proof by the actor's DID. Keys at compatible identifiers, and keys at ordinary URLs that claim a portable owner, are no longer trusted by origin.Context as their options, such as await create.getObject(ctx), now verify the portable objects they dereference, including references to compatible identifiers from objects with ordinary HTTP(S) IDs./.well-known/apgateway/, and Context.sendActivity() may reject an activity that embeds others' portable objects. Next.js applications need the matcher shown above to serve hashlink media.maxHttpSignatures if you need more.fedify lookup does too.Tombstone or Object that return tombstones now respond with 410 Gone instead of 200 OK.PostgresKvStore migrates existing unlogged tables to logged tables on initialization, which locks the table.RedisKvStore needs EVAL permission for cas().Context and RequestContext interfaces need the new getPortable*Uri() methods and isSignedByAudience().fedify tunnel no longer supports localhost.run.Thanks to this release's contributors:
@fedify/adonisjs, and the request for FEP-7aa9 vocabularytest task in fedify initfedify initactor-preferred-username-required and outbox-listener-delivery-not-awaited lint rules, and control-flow analysis for outbox-listener-delivery-requiredChanHaeng Lee (@2chanhaeng) and Jiwon Kwon (@z9mb1) joined as maintainers after 2.3.0. ChanHaeng built the background task API and @fedify/pglite, Jiwon built NetlifyBlobsKvStore and rewrote the relay documentation, and both reviewed much of the rest of this release.
Many people added tests, documentation, and infrastructure during this cycle, often starting from a good first issue: Seoa Yoon (@bananamilk452), Lim Kyoujin (@dktsudgg), @m2nsp, Cho Seongmin (@seongmin36), Safal Shrestha (@Safal-Shrestha-SS), Jo Youngjae (@jaonz6057), @kecan0406, Loi Nguyen (@lntutor), Oh Taejun (@0w0), Yerin Park (@dstforye), Lee Dogeon (@moreal), and Sae Jin Kim (@siliconsjang).
The FEP-ef61 work owes a lot to people outside the project. @anotherdoesnm opened the request in #288. @silverpill, the author of FEP-ef61 and FEP-ae97, clarified the gateway trust model, reviewed the compound proof profile, and checked its test vectors. @dmitrizagidulin and @by_caballero weighed in on the profile's JSON-LD questions in #938. Testing against tootik and Mitra led to two of the interoperability fixes in this release.
See CHANGES.md for the complete changelog.
github.com
ActivityPub server framework in TypeScript. Contribute to fedify-dev/fedify development by creating an account on GitHub.
Fedify 2.4.0 is out.
The main addition is support for FEP-ef61 portable objects. A Fedify server can now host actors whose identity is a DID instead of a domain, serve their signed objects through /.well-known/apgateway/, and verify the portable objects of other servers. I tested it against tootik and Mitra, and wrote up where Fedify's profile differs from the FEP and what it leaves out.
The release also adds application-defined background tasks, the ActivityPub Media Upload extension, per-request inbox reports, 410 Gone for deleted objects, and four new packages (AdonisJS, Netlify, PGlite, and interaction controls).
Some defaults changed, so please read the upgrading section before you update, especially if you federate with software that uses FEP-ef61 compatible identifiers.
Release notes: https://hackers.pub/@fedify/2026/fedify-2-4.
Thanks to everyone who contributed to this cycle, and to @silverpill for patient answers about the FEP.
hackers.pub
Fedify is a TypeScript framework for building ActivityPub servers. It implements federation details such as HTTP Signatures, JSON-LD processing, WebFinger, inbox and outbox routing, and activity delivery.
Fedify 2.4.0 is a minor release with an unusually long changelog. The main addition is support for FEP-ef61 portable objects: a Fedify server can now host actors whose identity is a DID rather than a domain, serve and verify their signed objects, and consume the portable objects of other servers. The release also adds an API for application-defined background tasks, the ActivityPub Media Upload extension, a hook that reports how every inbox request was authenticated and handled, and 410 Gone tombstones for deleted objects. Several defaults change to limit what a hostile peer can make Fedify do. Four new packages ship with this version: @fedify/adonisjs, @fedify/interaction-controls, @fedify/netlify, and @fedify/pglite.
Most applications can upgrade without code changes, but a few of the new defaults are behavior changes. They are listed in the Upgrading section at the end.
An ActivityPub object's ID is an HTTP(S) URL on its server's domain. If that server shuts down, its IDs stop resolving, and an actor that moves elsewhere gets a new ID that everyone has to follow again. FEP-ef61, written by @silverpill of Mitra, proposes IDs that do not name a server at all. A portable ID is an ap: URI whose authority is a DID:
ap://did:key:z6MkrJVnaZkeFzdQyMZu1cgjg7k1pZZ6pvBQ7XJPt4swbTQ2/actor
The same object can be stored on several servers, called gateways, each of which serves it under /.well-known/apgateway/. Because no server owns the ID, a portable actor, activity, or object is authenticated only by its FEP-8b32 Object Integrity Proof, made with a key of the DID in its ID. A portable actor lists its gateways in a gateways property, and other servers deliver to its inbox through them.
Fedify 2.4.0 implements a profile of FEP-ef61 for gateways and consumers of portable objects. It makes two deliberate choices where the FEP or the specifications it rests on leave questions open, and it leaves out a few parts of the proposal; both are described below. The new Portable objects chapter of the manual walks through running portable actors from start to finish and then describes the profile as a whole, so that other implementers can tell what to expect from a Fedify server. The request came from @anotherdoesnm in #288, which tracked the work from a roadmap to the final pieces.
Portable objects are a foundation for nomadic identity, not nomadic identity itself. Fedify serves and verifies portable objects and forwards activities between gateways, but it does not copy actors or posts between servers, rotate keys, or move an actor to another DID. Full nomadic identity is still open in #413.
A portable actor starts with an Ed25519 key pair. Its DID is made from the public key with exportDidKey(), and whoever holds the private key can act as the actor, so store it with the actor and keep it safe:
import { generateCryptoKeyPair } from "@fedify/fedify";
import { exportDidKey } from "@fedify/vocab-runtime";
const { publicKey, privateKey } = await generateCryptoKeyPair("Ed25519");
const did = await exportDidKey(publicKey); // did:key:z6Mk...
// The verification method that the actor's proofs refer to:
const didKeyId = new URL(`${did}#${did.slice("did:key:".length)}`);
The actor dispatcher returns a portable actor as it would an ordinary one. Context.getPortableActorUri() and its siblings for inboxes, outboxes, collections, and objects build portable IDs from the dispatcher's paths and the DID, and signObject() signs the actor with the DID's key:
federation
.setActorDispatcher("/users/{identifier}", async (ctx, identifier) => {
const user = await findUser(identifier);
if (user == null) return null;
// A request through the gateway endpoint names the DID in its path,
// e.g., GET /.well-known/apgateway/did:key:z6Mk.../users/alice:
if (
ctx.portableRequest != null &&
ctx.portableRequest.authority !== user.did
) {
return null;
}
// This server's gateway keys for the actor (see below):
const keys = await ctx.getActorKeyPairs(identifier);
return await signObject(
new Person({
id: ctx.getPortableActorUri(identifier, user.did), // ap+ef61://did:key:z6Mk.../users/alice
preferredUsername: user.username,
inbox: ctx.getPortableInboxUri(identifier, user.did),
outbox: ctx.getPortableOutboxUri(identifier, user.did),
followers: ctx.getPortableFollowersUri(identifier, user.did),
gateways: [new URL("https://example.com")],
publicKeys: keys.map((key) => key.cryptographicKey),
assertionMethods: keys.map((key) => key.multikey),
}),
user.didKeyPair.privateKey,
new URL(`${user.did}#${user.did.slice("did:key:".length)}`),
);
})
.setKeyPairsDispatcher(async (ctx, identifier) => {
const user = await findUser(identifier);
// This server's own key pair for the actor, not the DID's:
return user == null ? [] : [user.gatewayKeyPair];
})
.mapPortableActorId(async (ctx, identifier) => {
const user = await findUser(identifier);
return user == null ? null : ctx.getPortableActorUri(identifier, user.did);
});
With this dispatcher, Fedify serves the actor at its compatible identifier, https://example.com/.well-known/apgateway/did:key:z6Mk.../users/alice, which is how other servers retrieve it. It serves it through WebFinger as @alice@example.com, since the handle's domain comes from the first gateway in gateways, and at its ordinary URL as before. On the gateway endpoint, Fedify serves a document only if its ID is the requested portable ID and it carries a proof made with a key of that DID.
The key pairs dispatcher and the new ActorCallbackSetters.mapPortableActorId() give the actor gateway keys. These are the server's own keys for the actor, and they sign only the HTTP requests the server makes on the actor's behalf, such as deliveries. They never sign the actor's activities or objects; only the DID's key does that. The actor document lists them under assertionMethods, as FEP-521a describes, so that other servers can verify the HTTP Signatures. See Dispatching the actor and Gateway keys of portable actors for the details.
Object and collection dispatchers serve portable objects and collections through the gateway endpoint in the same way, routed by the path after the DID. Two rules protect the actor's data. A non-public portable object is served only if the dispatcher has an authorization predicate, because FEP-ef61 forbids a gateway to show it to anyone outside its audience; the new RequestContext.isSignedByAudience() helps to write one. Without a predicate, a request for such an object gets 404 Not Found, as though the gateway did not store it. A deleted object is served as a signed Tombstone with 410 Gone. Resources that portable objects refer to by SHA-256 hashlinks, such as GET /.well-known/apgateway/hl:zQm..., are served by the new Federatable.setHashlinkMediaDispatcher().
A portable actor's activities have to be signed by its DID, which Fedify's key pairs dispatcher does not know about. Sign them with signObject() before sending, and pass normalizeExistingProofs: true so that the activity goes out in the form its proof covers. An activity that embeds a portable object, such as the Note of a Create, is signed inside out:
const actor = withGatewayHints(
ctx.getPortableActorUri(user.identifier, user.did),
[new URL("https://example.com")],
); // ap+ef61://did:key:z6Mk.../users/alice?@gateway=https%3A%2F%2Fexample.com
const note = await signObject(
new Note({
id: ctx.getPortableObjectUri(
Note,
{ identifier: user.identifier, id: crypto.randomUUID() },
user.did,
),
attribution: actor,
to: PUBLIC_COLLECTION,
content: "Hello, world!",
}),
user.didKeyPair.privateKey,
didKeyId,
);
const create = await signObject(
new Create({
id: ctx.getPortableObjectUri(
Note,
{ identifier: user.identifier, id: crypto.randomUUID() },
user.did,
),
actor,
to: PUBLIC_COLLECTION,
object: note,
}),
user.didKeyPair.privateKey,
didKeyId,
);
await ctx.sendActivity(
{ identifier: user.identifier },
"followers",
create,
{ normalizeExistingProofs: true },
);
This example depends on a fix to signObject() that is useful outside FEP-ef61 as well. Previously, a signed object assigned to a typed parent was rebuilt under the parent's JSON-LD context when the parent was serialized. The child kept its proofValue but lost the document its proof covered, so it no longer verified on its own. signObject() now captures the exact document it signed, and nested serialization embeds that document verbatim. The captured document is a snapshot: clone() does not carry it, so sign a clone again if it has to be embedded. The design is in #1044 and the fix in #1051.
Context.sendActivity() refuses a portable activity that is unsigned, signed by the wrong key, or carries a Linked Data Signature, and throws a TypeError before anything is queued. It also refuses a few shapes of compound document that Fedify's own inboxes would reject, such as a received signed Follow embedded in an Accept; refer to such an object by its ID instead. Deliveries to portable inboxes go through the recipient's gateways, or else the @gateway hints on the inbox, trying at most five of them until one accepts. Before this release, they failed with UrlError: Unsupported protocol: ap+ef61:. The rules are in Delivering to portable actors and Producing a compound document.
On the receiving side, a portable inbox is reached as POST /.well-known/apgateway/did:key:z6Mk.../users/alice/inbox and dispatched to the same inbox listeners as the ordinary inbox. Context.parseUri() gains a portable option, and the parsed result has the DID in a new authority property to check against the one you store. Fedify accepts an activity of a portable actor, or one with a portable ID, only if it has a valid proof by that DID; neither an HTTP Signature nor a Linked Data Signature authenticates it. Each portable object embedded in the activity is verified against its own proof before dispatch. An accepted activity that carries its own proof is forwarded to the actor's other gateways at most once, as FEP-ef61 recommends. Forwarding needs a key–value store that supports cas(), and the FederationOptions.portableInboxForwarding option tunes it. See Portable inboxes and Forwarding to other gateways.
Applications that never host a portable actor still meet portable objects from other servers. Context.lookupObject() resolves portable IDs, compatible identifiers, and the fediverse handles of portable actors, and returns portable objects only if they verify:
const actor = await ctx.lookupObject("@alice@example.com");
// A portable ID without @gateway hints needs gateways to ask:
const note = await ctx.lookupObject("ap://did:key:z6Mk.../notes/1", {
gateways: ["https://example.com"],
});
Property accessors fetch portable references through gateways as well, and they verify what they fetch with the new verifyPortableObject option. Context has a matching verifyPortableObject property, so passing a context as the options, as in await create.getObject(ctx), is enough. Activities received in inboxes use this verifier by default, and objects fetched through their accessors inherit it, so a chained call such as (await create.getObject(ctx))?.getAttribution() verifies each portable object along the way. A lookup asks at most five gateways, falling back to the next when one fails or serves something that does not verify. Unsigned collections are accepted only from a gateway that the owner lists, and only for an actor's inbox, outbox, followers, following, and liked, as FEP-ef61 allows. fetchPortableMedia() retrieves the media of a portable object and returns it only if it matches the object's digestMultibase. See Dereferencing portable references and Default verifiers.
Software that does not understand ap: URIs can refer to a portable object by a compatible identifier, an HTTP(S) URL under a gateway's /.well-known/apgateway/ path such as https://gw.example/.well-known/apgateway/did:key:z6Mk.../actor. Some implementations, tootik among them, use compatible identifiers as the IDs of their portable actors.
Earlier versions of Fedify treated these URLs as ordinary web URLs and trusted them by the server that served them. Since anyone can serve a compatible identifier for any DID, https://evil.example/.well-known/apgateway/did:key:z6MkAlice/actor could act as Alice without a proof by Alice's DID. Fedify 2.4.0 treats a compatible identifier as the portable object it stands for, as FEP-ef61 requires. Inboxes reject an activity from an actor whose ID is a compatible identifier with 401 Unauthorized unless it has a valid Object Integrity Proof by the actor's DID, and there is no option to turn this off. Actors that publish compatible identifiers without signing them are no longer accepted, and neither are those whose DIDs use key types that Fedify cannot verify yet. The change was tracked in #1093 and made in #1105.
Your own portable actors may still use compatible identifiers as their IDs, for software that rejects ap: IDs. Build them with toCompatibleEf61Id() before signing, and Fedify will treat them as portable wherever it serves or sends them. It also warns if one is malformed or does not point at the actor's first gateway. See Compatible identifiers as actor IDs.
Two choices in Fedify's profile differ from the current FEP text or rest on semantics that are not settled yet. Fedify keeps both for the 2.x series, and either may change in Fedify 3.0.
The first is the scheme. FEP-ef61 recommends ap: and canonicalizes ap+ef61: to it, while also warning that the recommendation may change to ap+ef61:, since these IDs are meant only for portable objects. Fedify accepts both, compares them as equal, and serializes ap+ef61:. Parsing a document normalizes its portable IRIs to ap+ef61:, so re-serializing an object that another implementation signed with ap: IDs breaks its proof. Keep the received JSON of such documents, and compare portable IDs with arePortableUrisEqual() rather than as strings. The discussion is in #826 and #828, and the reasoning is in Canonical ap+ef61: scheme.
The second concerns compound documents. When a signed Note sits inside a signed Create, the verifier has to know which part of the JSON each proof covers, and neither FEP-8b32 nor Verifiable Credential Data Integrity defines that yet; the question is open in w3c/vc-data-integrity#350. Fedify uses an interim map-local profile, worked out in #938. Each JSON map with a proof is its own secured document, each embedded portable object needs its own proof and its own @context, and proof sets and chains are not supported. The profile authenticates the exact JSON values each proof covers; it does not promise that a child's JSON-LD expansion inside its parent matches its expansion on its own. @silverpill, @dmitrizagidulin, and @by_caballero reviewed the proposal in that thread, and @silverpill reproduced its test vectors independently. See Map-local compound proofs.
Fedify does not implement FEP-ae97, the companion proposal for client-side signing. There are no endpoints for registering actors, submitting client-signed activities to a portable outbox, or gateway discovery, and no media upload and deletion endpoints under /.well-known/apgateway-media. Your own outbox listeners can still relay activities that a client signed, unchanged. Fedify also does not copy or reconcile actors, objects, or collections between gateways, so running an actor on several gateways means keeping their data in sync yourself. There is no workflow for rotating a DID's key or migrating an actor to another DID. Fedify resolves verification methods only for did:key DIDs with Ed25519 keys; other DID methods verify only if your document loader resolves them. Gateways must be plain origins, since the arbitrary gateway paths that FEP-ef61 discusses are not supported. The full list is in Not supported.
The profile was tested against tootik v0.25.4 and Mitra v5.10.0 in #1202. Fedify verifies tootik's portable actors, activities, and HTTP Signatures, and Mitra's gateway HTTP Signatures on activities that a client signed. Both accept the portable activities that Fedify sends. Two fixes came out of this testing. Fedify now reads a gateways list that tootik leaves unmapped in its JSON-LD context. It also serializes gateways as bare origins, because Mitra rejects a trailing slash. Fedify has not yet verified an actor or activity that Mitra signed itself, and Mastodon's handling of portable actors has not been tested. The FEP-ef61 interoperability section of FEDERATION.md records what was tested and how.
fedify lookup looks up portable objects by their ap: and ap+ef61: IDs, compatible identifiers, and the handles of portable actors, and it verifies their proofs. When no gateway returns an acceptable object, it says why. A new --gateway option on fedify lookup, fedify inbox, and fedify webfinger names the gateways to ask. fedify inbox --follow follows portable actors, and fedify webfinger accepts portable actor IDs. See Looking up portable objects in the CLI manual.
The mock federation and contexts in @fedify/testing support portable IDs, verification against fixture document loaders, gateway requests, and hashlink media, so tests can exercise portable objects without a live gateway. @fedify/lint's actor URI rules accept the portable IDs that the getPortable*Uri() methods build. @fedify/next now passes hashlink media requests to Fedify, a problem reported in #1149 and fixed in #1170; to have Next.js run the middleware for them, add { source: "/.well-known/apgateway/:path*" } to your matcher.
Fedify already runs activity delivery and inbox processing on queue workers, but applications usually have jobs of their own: digest emails, timeline rebuilds, link previews. Until now, those jobs could not use Fedify's queues, serialization, or retries. Fedify 2.4.0 opens the same machinery to application-defined tasks.
A task is defined once with defineTask() and enqueued from any Context with enqueueTask() or enqueueTaskMany(). Every task takes a Standard Schema, such as a Zod or Valibot schema, which types its payload. Fedify validates the payload when it is enqueued and again when a worker picks it up; the second check catches payloads from an earlier deployment that no longer match the schema:
import { createExponentialBackoffPolicy } from "@fedify/fedify";
import { z } from "zod";
const sendDigest = federation.defineTask("sendDigest", {
schema: z.object({ userId: z.string(), since: z.date() }),
handler: async (ctx, data) => {
// data is typed as { userId: string; since: Date }
await sendEmail(data.userId, await buildDigest(data.userId, data.since));
},
retryPolicy: createExponentialBackoffPolicy({ maxAttempts: 3 }),
});
// Anywhere with a Context:
await ctx.enqueueTask(sendDigest, { userId: "alice", since: new Date() }, {
delay: { minutes: 30 },
deduplicationKey: "digest:alice",
});
Fedify serializes payloads with devalue, so Date, Map, Set, URL, bigint, Temporal values, circular references, and Activity Vocabulary objects all survive the trip through any message queue backend. A Note in a payload comes back as a Note. Failed handlers are retried with exponential backoff by default, and queues with native retries handle retries themselves. Tasks fall back to the outbox queue unless you give them their own. Use the new task slot of the queue option or a per-task queue for that, and startQueue(ctxData, { queue: "task" }) runs a task-only worker. Setting taskQueueResolution: "strict" turns the fallback into an error.
A deduplicationKey deduplicates enqueues. The feature came from @julian's point in #206 that recurring tasks are hard to manage when you cannot tell whether one is already queued. A queue that declares nativeDeduplication enforces the key itself; otherwise, Fedify keeps a best-effort marker in the key–value store through cas(). Each task runs in a fedify.task span, and the queue metrics from 2.3.0 now include task queues. The Background tasks chapter covers routing, retries, deduplication, and the limitations; cron-style scheduling, result backends, and priorities are out of scope for now.
ChanHaeng Lee (@2chanhaeng) implemented the API: the core in #803, deduplication in #806, and observability in #812, following the plans in #797, #798, and #799. The work was merged in #923, closing #206, which had been open since February 2025.
Fedify 2.2.0 added client-to-server outbox listeners, but a C2S client also has to upload images and video before it can post them. That traffic does not go through POST /outbox. Fedify 2.4.0 supports the ActivityPub Media Upload extension, a SocialCG proposal last updated in 2017 that never reached W3C Recommendation status.
setMediaUploader() registers a multipart/form-data endpoint. Its callback receives the uploaded file and the object shell the client posted along with it. If the callback returns the created object, Fedify responds with 201 Created; if it returns a URL where the object will appear once processing finishes, Fedify responds with 202 Accepted:
federation
.setMediaUploader(
"/users/{identifier}/media",
async (ctx, identifier, file, object) => {
const stored = await uploadToStorage(file);
return new Image({
id: ctx.getObjectUri(Image, { uuid: stored.uuid }),
url: new URL(stored.publicUrl),
mediaType: file.type,
name: object.name,
});
},
)
.authorize(async (ctx, identifier) => {
const session = await verifyAccessToken(
ctx.request.headers.get("authorization"),
);
return session?.identifier === identifier;
});
The actor dispatcher advertises the endpoint itself by setting endpoints: new Endpoints({ uploadMedia: ctx.getMediaUploaderUri(identifier) }), the same way it places its inbox and outbox URIs. Without an authorize() hook, anyone who finds the endpoint can upload to it. For that reason, Fedify logs a warning when an uploader is registered without one, and when the returned object's URI does not match an object dispatcher or the actor does not advertise the endpoint. Four new @fedify/lint rules catch the same mistakes before they run. Fedify reads the upload into memory and sets no size limit of its own, so cap the request size at your reverse proxy. Storing the file and serving the object back are up to the application. See the Media upload chapter; the work is in #754 and #927.
Fedify decides several things about every inbox delivery: which signature mechanisms applied, which keys they tried, whether the actor owned the key, and what happened to the activity. Before this release, applications saw only part of that. A Linked Data Signature by a key the actor did not own was counted the same as a bad signature, and the key that verified a request was never handed to the application. The request in #1191 came from an effort to log these decisions for a debugging tool.
The new onRequestFinished() callback on inbox listeners receives an InboxRequestReport for every delivery to an inbox route, including rejected requests and requests that fail before verification:
federation
.setInboxListeners("/users/{identifier}/inbox", "/inbox")
.on(Follow, async (ctx, follow) => { /* ... */ })
.onRequestFinished(async (ctx, report) => {
await db.insertInboxLog({
inbox: report.inbox.kind, // "personal" | "shared" | "portable"
authentication: report.authentication.status,
outcome: report.outcome.type === "response"
? report.outcome.disposition // "processed" | "enqueued" | "duplicate" | ...
: `exception:${report.outcome.stage}`,
keyIds: report.attempts
.flatMap((attempt) => attempt.checks)
.flatMap((check) => check.triedKeys)
.map((key) => key.id?.href),
});
});
Each HTTP Signature, Linked Data Signature, or Object Integrity Proof check appears as an attempt with the actual keys it tried, including a stale cached key and its refreshed replacement. The report also carries the final authentication decision, such as actorKeyMismatch or proofPolicy, and the response or exception that ended the request. Fedify awaits the callback whether or not the trace is sampled. Errors it throws are logged without changing the response, and enqueued means the queue accepted the activity, not that a worker later processed it. Mock federations in @fedify/testing accept the same callback. See Observing inbox requests; the implementation is in #1201.
Actor dispatchers have been able to return a Tombstone since 2.2.0, but object dispatchers could only return null. A deleted post was answered with 404 Not Found, as if it had never existed, so applications that wanted 410 Gone, as ActivityPub recommends, needed their own route in front of Fedify.
Object dispatchers may now return a Tombstone, which Fedify serves with 410 Gone and the serialized tombstone after applying the authorization predicate:
federation.setObjectDispatcher(
Note,
"/users/{identifier}/notes/{id}",
async (ctx, values) => {
const post = await getPost(values.identifier, values.id);
if (post == null) return null;
if (post.deletedAt != null) {
return new Tombstone({
id: ctx.getObjectUri(Note, values),
formerType: Note,
deleted: post.deletedAt,
});
}
return new Note({ id: ctx.getObjectUri(Note, values), content: post.content });
},
);
The return type of ObjectDispatcher widens to TObject | Tombstone | null. RequestContext.getObject() still returns null for a tombstone unless you pass { tombstone: "passthrough" } or ask for the Tombstone or Object class itself. Dispatchers registered for Tombstone or Object that already returned tombstones now respond with 410 rather than 200. See Deleted objects; the change was proposed in #1112 and made in #1117.
Much of what Fedify fetches is chosen by the sender of an incoming request. Key IDs in signatures are one example, and the servers behind them can be slow on purpose. Several changes in 2.4.0 bound that work.
As #1130 points out, a request may carry several RFC 9421 signatures, and each one can make Fedify fetch a key from a URL of the sender's choosing. Fedify now verifies at most the first three, in Signature-Input order, and ignores the rest. A signature counts toward the limit even if it fails before its key is fetched, and a key named by several signatures is looked up once. The new FederationOptions.maxHttpSignatures option, added with the limit in #1166, changes it.
The built-in document loaders now give each call ten seconds by default, a limit proposed in #1131 and added in #1169. The time limit covers every redirect, alternate document, double-knocking retry, and the response body. A timed-out call throws a FetchError whose cause is a DOMException named "TimeoutError". Error responses are read in full before the loader throws, up to 1 MiB, so that a server cannot send a 404 header and then stall the body. FederationOptions.documentLoaderTimeout changes the limit or turns it off with null, and custom document loaders are not affected. Since #913, getDocumentLoader() also rejects HTML responses that do not advertise an ActivityPub alternate with a FetchError, rather than failing inside JSON.parse(), which #912 found cluttering error trackers.
Cached public keys and remembered HTTP Message Signatures specs used to live forever in the key–value store. #1017 reports a production Redis instance holding 194,424 public-key entries, about 165 MiB, for actors that had mostly stopped federating. Keys now expire after 30 days and specs after 90, configurable through FederationOptions.publicKeyTtl and FederationOptions.httpMessageSignaturesSpecTtl. Entries written by earlier versions have no expiry until they are next written. The Clearing legacy cache entries section shows how to remove them for Redis, PostgreSQL, and other stores. Heewon Chae (@heeoneie) implemented this in #1027, their first contribution to Fedify.
const federation = createFederation<void>({
kv,
maxHttpSignatures: 1, // default: 3
documentLoaderTimeout: { seconds: 5 }, // default: 10 seconds
publicKeyTtl: { days: 7 }, // default: 30 days
httpMessageSignaturesSpecTtl: { days: 30 }, // default: 90 days
});
Finally, verifyProof() now authenticates every option of an Ed25519 JCS proof except proofValue, including expires, domain, challenge, and nonce. It also rejects expired or malformed ones. Previously, a field such as expires could be added to an already signed proof without invalidating it. Jiwon Kwon (@z9mb1) spotted this in review of #968. Callers can now require a domain and challenge through VerifyProofOptions to prevent cross-domain or replayed use.
@fedify/adonisjs@fedify/adonisjs integrates Fedify with AdonisJS. Running node ace add @fedify/adonisjs registers a service provider that owns the federation's lifecycle and a server middleware that mounts it. It also scaffolds config/fedify.ts, start/federation.ts, and app/federation/main.ts. Controllers get a ctx.federation request context:
// app/federation/main.ts
import federation from "@fedify/adonisjs/services/builder";
import { Person } from "@fedify/vocab";
federation.setActorDispatcher("/actors/{identifier}", (ctx, identifier) =>
new Person({ id: ctx.getActorUri(identifier), preferredUsername: identifier }));
The middleware sits first in AdonisJS's server stack rather than its router stack. That way Fedify sees paths the router does not know, such as /.well-known/webfinger, and reads request bodies before the body parser consumes them. The package targets Node.js and is published to npm only. See the AdonisJS section of the integration guide.
Samuel Brinkmann (@sabrinkmann) contributed the package in #1006, closing #139, and built it on a prototype that Emelia Smith (@thisismissem) attached to that issue.
npm add @fedify/adonisjs
pnpm add @fedify/adonisjs
yarn add @fedify/adonisjs
@fedify/interaction-controlsGoToSocial interaction controls, FEP-044f quote authorization, and FEP-7aa9 featured collections share a request-and-authorization flow. A server asks to reply to, quote, or feature a post, and the author's server approves according to the post's interactionPolicy. @fedify/interaction-controls implements that flow for like, reply, announce, quote, and feature interactions. Each has a helper, such as likeInteraction or quoteInteraction, that creates and verifies requests and authorizations, evaluates policies, and recognizes interactions that arrived without a request:
import { likeInteraction } from "@fedify/interaction-controls";
// When a LikeRequest arrives:
const verified = await likeInteraction.verifyRequest(ctx, { request });
if (verified.verified) {
const decision = await likeInteraction.evaluatePolicy(ctx, {
subject: note,
requester: request.actorId!,
});
if (decision.result === "automatic") { // or "manual" or "denied"
const authorization = likeInteraction.createAuthorization({
id: new URL("https://example.com/authorizations/1"),
attributedTo: owner,
interactingObject: like,
interactionTarget: note,
});
// ...
}
}
The package does not install inbox listeners or store anything; the application decides where requests and authorizations live and when a human needs to approve. Verification fails closed. Without a policy, likes, replies, and announces are approved automatically, while quotes and features are denied. @fedify/vocab gains the FEP-7aa9 types this needs: FeaturedCollection, FeaturedItem, FeatureRequest, and FeatureAuthorization, along with actors' featuredCollections and InteractionPolicy.canFeature. Samuel Brinkmann asked for them in #810 once Mastodon 4.6 shipped the FEP. See the Interaction controls chapter; the work is in #914 and #929.
npm add @fedify/interaction-controls
pnpm add @fedify/interaction-controls
yarn add @fedify/interaction-controls
deno add jsr:@fedify/interaction-controls
bun add @fedify/interaction-controls
@fedify/netlify@fedify/netlify lets a Fedify application run on Netlify Functions without an external database or queue. NetlifyMessageQueue submits jobs as Netlify Async Workloads events, and createNetlifyQueueHandler() turns a Function into their consumer, with delayed delivery, native retries, and per-key FIFO ordering. NetlifyBlobsKvStore is a key–value store on Netlify Blobs with expiration, prefix listing, and compare-and-swap:
import {
createNetlifyQueueHandler,
NetlifyBlobsKvStore,
NetlifyMessageQueue,
} from "@fedify/netlify";
import { AsyncWorkloadsClient } from "@netlify/async-workloads";
import { getStore } from "@netlify/blobs";
export const kv = new NetlifyBlobsKvStore(
getStore({ name: "fedify", consistency: "strong" }),
);
export const queue = new NetlifyMessageQueue({
client: new AsyncWorkloadsClient(),
orderingKv: kv,
});
// netlify/functions/fedify-queue.ts
export default createNetlifyQueueHandler({
queue,
maxRetries: 4,
federation: () => builder.build({ kv, queue, manuallyStartQueue: true }),
});
The key–value store exists because of cost. #1010 describes a static blog whose ActivityPub traffic kept a Netlify Database from ever idling, which used most of the account's monthly credits. Jiwon Kwon contributed NetlifyBlobsKvStore in #1029, and the queue landed in #934. See Netlify Functions in the deployment guide.
npm add @fedify/netlify
pnpm add @fedify/netlify
yarn add @fedify/netlify
deno add jsr:@fedify/netlify
bun add @fedify/netlify
@fedify/pglite@fedify/pglite provides PgliteKvStore, a KvStore on an embedded PGlite database that you create and pass in. It uses the same schema as PostgresKvStore. It is meant for a single PGlite instance in a single runtime isolate, so it does not include a message queue: PGlite does not share data between processes.
import { PGlite } from "@electric-sql/pglite";
import { PgliteKvStore } from "@fedify/pglite";
const federation = createFederation({
kv: new PgliteKvStore(new PGlite("./data/fedify")),
});
ChanHaeng Lee contributed the package in #1020, together with testKvStore() in @fedify/testing. testKvStore() is a conformance suite for KvStore implementations, the counterpart to testMessageQueue(). See PgliteKvStore.
npm add @fedify/pglite
pnpm add @fedify/pglite
yarn add @fedify/pglite
deno add jsr:@fedify/pglite
bun add @fedify/pglite
Portable inbox forwarding, task deduplication, Netlify queue ordering, and the delivery circuit breaker all need an atomic compare-and-swap on the key–value store. PostgresKvStore now implements cas(), added in #934, and so does RedisKvStore, requested in #1163 and added in #1167. The Redis version uses a single-key Lua script, which works on standalone Redis and Redis Cluster. Before this, the circuit breaker fell back to racy updates on Redis.
Both come with conditions. RedisKvStore needs Redis to permit EVAL, and custom codecs must encode equal values identically. PostgresKvStore now creates logged tables by default, because losing sequence state in crash recovery could block queued events for good. It migrates existing unlogged tables when it initializes. That one-time migration rewrites the table and locks it exclusively, so schedule the upgrade accordingly if the table is large or busy; unlogged: true keeps the old behavior.
fedify init and the CLIfedify init can now scaffold a SvelteKit project, contributed by Jang Hanarae (@menele) in #971. Jang Hanarae also added a test task to generated projects in #990. It starts the app, waits for it to be ready, and checks that it resolves a local actor, which gives new projects a smoke test to run after scaffolding and after later changes. Before generating anything, fedify init now checks that the selected Deno, Bun, or Node.js meets Fedify's minimum version, or a framework's higher one, such as Astro's Node.js 22.12. It fails with a clear error in non-interactive mode, and in interactive mode it disables the affected package managers. Lee Jeongmin (@userjmmm) contributed the check in #981. Astro projects now use Astro 7, and since #936 @fedify/astro is tested against Astro 5, 6, and 7 with real builds on Node.js, Deno, and Bun, as #931 asked.
fedify tunnel gains fedify.com.es, a tunneling service run by the Fedify project that needs no account; the CLI pins its SSH host key and refuses a mismatched server before exposing a local port. localhost.run is removed in the same change, #940, because the service no longer exists. fedify lookup now times out each request after ten seconds when -T/--timeout is not given, and the option now also bounds the wait for a DNS lookup. The CLI moves to Optique 1.3.2 in #1197, which handles attached values like --timeout=30 consistently, gives better suggestions for mistyped options, and no longer hides errors for known options behind positional arguments before --.
@fedify/lint gains several rules beyond the media upload ones mentioned above. actor-preferred-username-required, requested in #895 and added in #1022, warns when an actor dispatcher returns an actor without preferredUsername. outbox-listener-delivery-not-awaited, from #1057 and #1067, reports an outbox listener that calls ctx.sendActivity() or ctx.forwardActivity() and drops the promise. On Cloudflare Workers, such an activity may never leave, because pending work is discarded once the response returns. await, return, Promise.all(), Promise.allSettled(), and waitUntil() count as handling the promise, while void, Promise.race(), and Promise.any() are accepted as a deliberate choice not to wait. The rule is a warning in the ESLint recommended configuration and an error in strict. Oxlint users enable it by name, and it is not available in Deno Lint.
In #1050, which resolves #900, the existing outbox-listener-delivery-required rule began following control flow instead of scanning the listener's source as text. It reports a listener whose only delivery calls sit in a dead branch, after an unconditional return or throw, or in a function that is never used, and it stays quiet when it cannot tell. Jae-Hyuk-Jang (@jhyuk) contributed this change and both new rules. archie (@ArchieTansaria) then taught both delivery rules about statically false loops and loop binding patterns in #1088. See the linting guide.
@fedify/vocab supports the FEP-22cd draft for translations. A Translation records a translated version's language, translators, source object, and source review time, and the new Object.translations property attaches them to a post without creating separate posts. See Translation metadata. (#1037, #1038)@fedify/vocab-tools gains an extraContext property option, which adds a JSON-LD context only when its terms are used, and a trustEmbeddedObjects type option. (#1037, #1038)suppressError: true handles are now logged as warnings rather than errors, so that they no longer show up as application errors. Contributed by Jae Hui Hong (@axz0612). (#933, #1035)enqueueMany() makes separate sends can set MessageQueue.atomicEnqueueMany to false. Fedify then rejects a multi-message batch governed by one deduplicationKey up front, since a partial send followed by a retry could enqueue duplicates. (#930, #934)Temporal bundle temporal-polyfill instead of requiring @js-temporal/polyfill at runtime, and type declarations rely on the standard esnext.temporal lib. (#823, #925)Accept header now includes the ActivityStreams profile on the JSON-LD media type, as ActivityPub and FEP-ef61 gateways require. (#834, #1077)fedify lookup --recurse reporting a timeout or other network failure as a possibly private object; it now reports the actual cause. (#1156, #1199)-p/--allow-private-address option of fedify webfinger being ignored. (#1156, #1199)outbox-listener-delivery-required and outbox-listener-delivery-not-awaited losing track of an object's functions when a nested helper assigns another property of that object. (#1125, #1140)npm add @fedify/fedify@2.4.0
pnpm add @fedify/fedify@2.4.0
yarn add @fedify/fedify@2.4.0
deno add jsr:@fedify/fedify@2.4.0
bun add @fedify/fedify@2.4.0
Update the other @fedify/* packages you use to 2.4.0 at the same time. To update the CLI, run npm install -g @fedify/cli@2.4.0, or see the installation instructions for other package managers.
Most applications need no code changes, but these changes in behavior may affect you:
401 Unauthorized unless they carry a valid Object Integrity Proof by the actor's DID. Keys at compatible identifiers, and keys at ordinary URLs that claim a portable owner, are no longer trusted by origin.Context as their options, such as await create.getObject(ctx), now verify the portable objects they dereference, including references to compatible identifiers from objects with ordinary HTTP(S) IDs./.well-known/apgateway/, and Context.sendActivity() may reject an activity that embeds others' portable objects. Next.js applications need the matcher shown above to serve hashlink media.maxHttpSignatures if you need more.fedify lookup does too.Tombstone or Object that return tombstones now respond with 410 Gone instead of 200 OK.PostgresKvStore migrates existing unlogged tables to logged tables on initialization, which locks the table.RedisKvStore needs EVAL permission for cas().Context and RequestContext interfaces need the new getPortable*Uri() methods and isSignedByAudience().fedify tunnel no longer supports localhost.run.Thanks to this release's contributors:
@fedify/adonisjs, and the request for FEP-7aa9 vocabularytest task in fedify initfedify initactor-preferred-username-required and outbox-listener-delivery-not-awaited lint rules, and control-flow analysis for outbox-listener-delivery-requiredChanHaeng Lee (@2chanhaeng) and Jiwon Kwon (@z9mb1) joined as maintainers after 2.3.0. ChanHaeng built the background task API and @fedify/pglite, Jiwon built NetlifyBlobsKvStore and rewrote the relay documentation, and both reviewed much of the rest of this release.
Many people added tests, documentation, and infrastructure during this cycle, often starting from a good first issue: Seoa Yoon (@bananamilk452), Lim Kyoujin (@dktsudgg), @m2nsp, Cho Seongmin (@seongmin36), Safal Shrestha (@Safal-Shrestha-SS), Jo Youngjae (@jaonz6057), @kecan0406, Loi Nguyen (@lntutor), Oh Taejun (@0w0), Yerin Park (@dstforye), Lee Dogeon (@moreal), and Sae Jin Kim (@siliconsjang).
The FEP-ef61 work owes a lot to people outside the project. @anotherdoesnm opened the request in #288. @silverpill, the author of FEP-ef61 and FEP-ae97, clarified the gateway trust model, reviewed the compound proof profile, and checked its test vectors. @dmitrizagidulin and @by_caballero weighed in on the profile's JSON-LD questions in #938. Testing against tootik and Mitra led to two of the interoperability fixes in this release.
See CHANGES.md for the complete changelog.
github.com
ActivityPub server framework in TypeScript. Contribute to fedify-dev/fedify development by creating an account on GitHub.
Es gibt @bizzfed, @vutuv und mindestens noch ein Fediverse-gekoppeltes Business-Netzwerk („LinkedIn-Alternative“), dessen Namen ich vergessen habe.
Weiß jemand, wie die sich entwickeln? So zahlen- und aktivitätsmäßig? #ActivityPub #LinkedIn
(Die üblichen „Es ist Fedi, es ist egal weil alles gekoppelt“ und „Zahlen sind mir egal“ brauchen nicht antworten, danke.)
Blocks on the fediverse have a consistency problem.
When a Block or Undo Block gets lost between servers, the two sides quietly disagree about who blocked whom. Sometimes that means a blocked account can still reach you.
Our fix, FEP-070c: Block synchronization across servers, is now accepted as a draft. Pixelfed is the first implementation, merged and shipping in our next release.
https://codeberg.org/fediverse/fep/src/branch/main/fep/070c/fep-070c.md
codeberg.org
Blocks on the fediverse have a consistency problem.
When a Block or Undo Block gets lost between servers, the two sides quietly disagree about who blocked whom. Sometimes that means a blocked account can still reach you.
Our fix, FEP-070c: Block synchronization across servers, is now accepted as a draft. Pixelfed is the first implementation, merged and shipping in our next release.
https://codeberg.org/fediverse/fep/src/branch/main/fep/070c/fep-070c.md
codeberg.org
Blocks on the fediverse have a consistency problem.
When a Block or Undo Block gets lost between servers, the two sides quietly disagree about who blocked whom. Sometimes that means a blocked account can still reach you.
Our fix, FEP-070c: Block synchronization across servers, is now accepted as a draft. Pixelfed is the first implementation, merged and shipping in our next release.
https://codeberg.org/fediverse/fep/src/branch/main/fep/070c/fep-070c.md
codeberg.org
Blocks on the fediverse have a consistency problem.
When a Block or Undo Block gets lost between servers, the two sides quietly disagree about who blocked whom. Sometimes that means a blocked account can still reach you.
Our fix, FEP-070c: Block synchronization across servers, is now accepted as a draft. Pixelfed is the first implementation, merged and shipping in our next release.
https://codeberg.org/fediverse/fep/src/branch/main/fep/070c/fep-070c.md
codeberg.org
How should we support #ActivityPub creators who want to see their engagement metrics, while ensuring user privacy?
I've got some ideas (https://rochebit.net/posts/analytics), but I'd really love to hear some other perspectives and approaches
rochebit.net
I’ve been spending a lot of time thinking about analytics on the open social web. I would love to hear how others approach this topic, and share my own perspective.
Pixelfed has never federated Blocks, which can cause some issues, so we came up with a Block Synchronization FEP that is heavily inspired by the Mastodon Follower Collection Sync FEP!
Blocks will soon federate, and Pixelfed will be the first implementation to support this Block Sync FEP.
github.com
This PR adds support for incoming and outgoing Block activities, and a mechanism to sync blocks across supported servers, based on the Follower Sync FEP. Review FEP-070c here
Pixelfed has never federated Blocks, which can cause some issues, so we came up with a Block Synchronization FEP that is heavily inspired by the Mastodon Follower Collection Sync FEP!
Blocks will soon federate, and Pixelfed will be the first implementation to support this Block Sync FEP.
github.com
This PR adds support for incoming and outgoing Block activities, and a mechanism to sync blocks across supported servers, based on the Follower Sync FEP. Review FEP-070c here
Wrote and submitted my first FEP!
FEP-070c: Block synchronization across servers
Just wrapping up the implementation in Pixelfed as we speak.
github.com
This PR adds support for incoming and outgoing Block activities, and a mechanism to sync blocks across supported servers, based on the Follower Sync FEP. Review FEP-070c here
Wrote and submitted my first FEP!
FEP-070c: Block synchronization across servers
Just wrapping up the implementation in Pixelfed as we speak.
github.com
This PR adds support for incoming and outgoing Block activities, and a mechanism to sync blocks across supported servers, based on the Follower Sync FEP. Review FEP-070c here
Hola! We're Protopia: a regenerative farm and co-living lab on 14 hectares of jungle in Costa Rica.
Artists, activists and digital nomads live, work and have fun together: sociocratic governance, pro social tech, evidence based thinking.
Our members work on ActivityPub tools, which is partly why we're here. If you do too, reach out, and come and work from here.
protopia.community
#Introduction #CoLiving #Regenerative #CostaRica #activitypub #community
Can someone explain to me why was Pixelfed, Loops, and Dansup being singled out for AI use?
There's no shortage of Fediverse software that are either built with AI assistance (including vibe coding), or welcomes AI contribution, but I'm not seeing the same level of outrage.
Did Pixelfed, Loops, and Dansup specifically promise to be AI-free?
Can someone explain to me why was Pixelfed, Loops, and Dansup being singled out for AI use?
There's no shortage of Fediverse software that are either built with AI assistance (including vibe coding), or welcomes AI contribution, but I'm not seeing the same level of outrage.
Did Pixelfed, Loops, and Dansup specifically promise to be AI-free?
Bij ProcoliX draait alles om digitale soevereiniteit en opensource-infrastructuur. Daarom bouwen we ProfessionalNetworking.eu: een gefedereerd alternatief voor netwerken.
🌐 Gefedereerd. Het platform spreekt ActivityPub, zodat mensen elkaar op verschillende servers kunnen vinden en volgen.
💼 Professioneel. Profielen, connecties, communities, organisatiepagina's, cv's, vacatures en evenementen.
🔓 Open by design. Open standaarden en een opensourcestack, zonder lock-in.
Het platform wordt ontworpen en gebouwd als stageproject bij ProcoliX. De architectuurfase is afgerond en de eerste werkende gefedereerde functies zijn de volgende stap.
Volg het project voor updates 👇
https://mastodon.social/@ProfessionalNetworkingEU
#ProcoliX #digitalesoevereiniteit #opensource #ActivityPub #fediverse #betterthanbigtech
mastodon.social
7 Posts, 2 Following, 20 Followers · ProfessionalNetworking.eu is an open-source, federated alternative to other Big Tech Networking platforms built in Europe, governed by its community, and designed so your professional network always belongs to you. OPEN. FEDERATED. YOURS.
@berlinfediday @julian I did not know about SciOp.
Great project and they are making a #FEP (https://fediverse.codeberg.page/fep/fep/d8c8/) to allow #BitTorrent data types in #activitypub - cool!
helge.codeberg.page
The BitTorrent protocol is a p2p protocol for distributing data described as a series of hashes and file metadata contained in .torrent files. This FEP describes a JSON-LD representation of .torrent files as an extension of an ActivityStreams Object.
@berlinfediday @julian I did not know about SciOp.
Great project and they are making a #FEP (https://fediverse.codeberg.page/fep/fep/d8c8/) to allow #BitTorrent data types in #activitypub - cool!
helge.codeberg.page
The BitTorrent protocol is a p2p protocol for distributing data described as a series of hashes and file metadata contained in .torrent files. This FEP describes a JSON-LD representation of .torrent files as an extension of an ActivityStreams Object.
Bij ProcoliX draait alles om digitale soevereiniteit en opensource-infrastructuur. Daarom bouwen we ProfessionalNetworking.eu: een gefedereerd alternatief voor netwerken.
🌐 Gefedereerd. Het platform spreekt ActivityPub, zodat mensen elkaar op verschillende servers kunnen vinden en volgen.
💼 Professioneel. Profielen, connecties, communities, organisatiepagina's, cv's, vacatures en evenementen.
🔓 Open by design. Open standaarden en een opensourcestack, zonder lock-in.
Het platform wordt ontworpen en gebouwd als stageproject bij ProcoliX. De architectuurfase is afgerond en de eerste werkende gefedereerde functies zijn de volgende stap.
Volg het project voor updates 👇
https://mastodon.social/@ProfessionalNetworkingEU
#ProcoliX #digitalesoevereiniteit #opensource #ActivityPub #fediverse #betterthanbigtech
mastodon.social
7 Posts, 2 Following, 20 Followers · ProfessionalNetworking.eu is an open-source, federated alternative to other Big Tech Networking platforms built in Europe, governed by its community, and designed so your professional network always belongs to you. OPEN. FEDERATED. YOURS.
@berlinfediday @julian I did not know about SciOp.
Great project and they are making a #FEP (https://fediverse.codeberg.page/fep/fep/d8c8/) to allow #BitTorrent data types in #activitypub - cool!
helge.codeberg.page
The BitTorrent protocol is a p2p protocol for distributing data described as a series of hashes and file metadata contained in .torrent files. This FEP describes a JSON-LD representation of .torrent files as an extension of an ActivityStreams Object.
It appears that FedWiki has completed some kind of ActivityPub implementation;
https://github.com/fedwiki/wiki/issues/122#event-31977514814
Anyone know more about this?
github.com
Example use case: I want to run be able to publish a blog piece using an instance of a fediverse blogging server like Hubzilla, Plume, or WriteAs, and have a copy of that text automatically added a...
I decided to donate €527.04 to GoToSocial (€509.66 is going to GoToSocial, and €17.38 in processing fees), lump sum.
techhub.social
Anyone working on a Fediverse server where all new code isn't written by AI? Do you have some Patreon, GitHub Sponsorship setup, Ko-Fi, Liberapay, or anything to accept donations? Happy to fork over $50 every month for the next year. #Fediverse #ActivityPub
Hola! We're Protopia: a regenerative farm and co-living lab on 14 hectares of jungle in Costa Rica.
Artists, activists and digital nomads live, work and have fun together: sociocratic governance, pro social tech, evidence based thinking.
Our members work on ActivityPub tools, which is partly why we're here. If you do too, reach out, and come and work from here.
protopia.community
#Introduction #CoLiving #Regenerative #CostaRica #activitypub #community
I decided to donate €527.04 to GoToSocial (€509.66 is going to GoToSocial, and €17.38 in processing fees), lump sum.
techhub.social
Anyone working on a Fediverse server where all new code isn't written by AI? Do you have some Patreon, GitHub Sponsorship setup, Ko-Fi, Liberapay, or anything to accept donations? Happy to fork over $50 every month for the next year. #Fediverse #ActivityPub
I decided to donate €527.04 to GoToSocial (€509.66 is going to GoToSocial, and €17.38 in processing fees), lump sum.
techhub.social
Anyone working on a Fediverse server where all new code isn't written by AI? Do you have some Patreon, GitHub Sponsorship setup, Ko-Fi, Liberapay, or anything to accept donations? Happy to fork over $50 every month for the next year. #Fediverse #ActivityPub
Progress update on ProfessionalNetworking.eu
An open, federated alternative to LinkedIn, built on ActivityPub, so you can find and follow people across servers instead of inside one company's platform.
Phase 1 is done: dev environment, C4 architecture model, a 17-entity data model and 8 design decisions (ADRs). I'm building on Rails and the Fedipub gem.
Next: database schema, then the first federated slice: follow, posts and boosts.
The wanderer app is now in public beta for iOS and Android!
Your self-hosted trail database, in your pocket:
- Find trails across federated instances
- Plan routes on the map
- Record hikes with GPS, no network needed
- Download map regions for offline use
- Turn-by-turn navigation, even offline
- Recordings sync as soon as you're back in range
Admins: the app needs the v0.21.0-app images. Setup guide: https://wanderer.to/run/backend-configuration/mobile-app
It's a beta, so expect rough edges and always carry a backup map. Install via TestFlight, Google Play or APK:
https://wanderer.to/app
#wanderer #selfhosted #hiking #FOSS #ActivityPub #fediverse #OpenStreetMap
The wanderer app is now in public beta for iOS and Android!
Your self-hosted trail database, in your pocket:
- Find trails across federated instances
- Plan routes on the map
- Record hikes with GPS, no network needed
- Download map regions for offline use
- Turn-by-turn navigation, even offline
- Recordings sync as soon as you're back in range
Admins: the app needs the v0.21.0-app images. Setup guide: https://wanderer.to/run/backend-configuration/mobile-app
It's a beta, so expect rough edges and always carry a backup map. Install via TestFlight, Google Play or APK:
https://wanderer.to/app
#wanderer #selfhosted #hiking #FOSS #ActivityPub #fediverse #OpenStreetMap
The wanderer app is now in public beta for iOS and Android!
Your self-hosted trail database, in your pocket:
- Find trails across federated instances
- Plan routes on the map
- Record hikes with GPS, no network needed
- Download map regions for offline use
- Turn-by-turn navigation, even offline
- Recordings sync as soon as you're back in range
Admins: the app needs the v0.21.0-app images. Setup guide: https://wanderer.to/run/backend-configuration/mobile-app
It's a beta, so expect rough edges and always carry a backup map. Install via TestFlight, Google Play or APK:
https://wanderer.to/app
#wanderer #selfhosted #hiking #FOSS #ActivityPub #fediverse #OpenStreetMap
Progress update on ProfessionalNetworking.eu
An open, federated alternative to LinkedIn, built on ActivityPub, so you can find and follow people across servers instead of inside one company's platform.
Phase 1 is done: dev environment, C4 architecture model, a 17-entity data model and 8 design decisions (ADRs). I'm building on Rails and the Fedipub gem.
Next: database schema, then the first federated slice: follow, posts and boosts.
Es gibt @bizzfed, @vutuv und mindestens noch ein Fediverse-gekoppeltes Business-Netzwerk („LinkedIn-Alternative“), dessen Namen ich vergessen habe.
Weiß jemand, wie die sich entwickeln? So zahlen- und aktivitätsmäßig? #ActivityPub #LinkedIn
(Die üblichen „Es ist Fedi, es ist egal weil alles gekoppelt“ und „Zahlen sind mir egal“ brauchen nicht antworten, danke.)
If I made a Federated Search system, and it required 10 gigabytes on a server and 1 gb on a PC using it (caches). Would you host this? Would you use it?
If I made a Federated Search system, and it required 10 gigabytes on a server and 1 gb on a PC using it (caches). Would you host this? Would you use it?
I decided to donate €527.04 to GoToSocial (€509.66 is going to GoToSocial, and €17.38 in processing fees), lump sum.
techhub.social
Anyone working on a Fediverse server where all new code isn't written by AI? Do you have some Patreon, GitHub Sponsorship setup, Ko-Fi, Liberapay, or anything to accept donations? Happy to fork over $50 every month for the next year. #Fediverse #ActivityPub
I decided to donate €527.04 to GoToSocial (€509.66 is going to GoToSocial, and €17.38 in processing fees), lump sum.
techhub.social
Anyone working on a Fediverse server where all new code isn't written by AI? Do you have some Patreon, GitHub Sponsorship setup, Ko-Fi, Liberapay, or anything to accept donations? Happy to fork over $50 every month for the next year. #Fediverse #ActivityPub
Anyone working on a Fediverse server where all new code isn't written by AI?
Do you have some Patreon, GitHub Sponsorship setup, Ko-Fi, Liberapay, or anything to accept donations?
Happy to fork over $50 every month for the next year.
Anyone working on a Fediverse server where all new code isn't written by AI?
Do you have some Patreon, GitHub Sponsorship setup, Ko-Fi, Liberapay, or anything to accept donations?
Happy to fork over $50 every month for the next year.
Looking to build a custom #ActivityPub bot or consume an external RSS feed in your Mastodon timeline? 🤖
You don't have to do it all by yourself. We have a network of elite Python, Go, and Ruby developers ready to create sleek, minimal open-source software and bots for activity streams.
Hire professional clean-code developers on Fiverr:(https://go.fiverr.com/visit/?bta=1174784&brand=fiverrmarketplace&landingPage=https%253A%252F%252Fwww.fiverr.com%252Fcategories%252Fprogramming-tech%252Fsoftware-development%253Fsource%253Ddrop_down_filters%2526ref%253Dseller_level%25253Atop_rated_seller%25252Clevel_two_seller)
CW : sponsored
Looking to build a custom #ActivityPub bot or consume an external RSS feed in your Mastodon timeline? 🤖
You don't have to do it all by yourself. We have a network of elite Python, Go, and Ruby developers ready to create sleek, minimal open-source software and bots for activity streams.
Hire professional clean-code developers on Fiverr:(https://go.fiverr.com/visit/?bta=1174784&brand=fiverrmarketplace&landingPage=https%253A%252F%252Fwww.fiverr.com%252Fcategories%252Fprogramming-tech%252Fsoftware-development%253Fsource%253Ddrop_down_filters%2526ref%253Dseller_level%25253Atop_rated_seller%25252Clevel_two_seller)
CW : sponsored
does anyone know how long #activitypub servers remember you?
after quite some time i have decided try to tinkering with some networking again (using the same domain i used before.)
just now while i was in the middle of doing some research i suddenly got a post fediverse request to my temporary python server. XD
does anyone know how long #activitypub servers remember you?
after quite some time i have decided try to tinkering with some networking again (using the same domain i used before.)
just now while i was in the middle of doing some research i suddenly got a post fediverse request to my temporary python server. XD
Yep, here's my presentation at Berlin FediDay 2026, where I talked about SciOp and its federation plans. 🙂
That was a really nice environment and audience for this exchange. Good Q&A too. I'm looking forward to the recording of the “research on the fediverse” panel at the end of the session.
berlin.social
🧪 @julian@fietkau.social — SciOp & decentralised research data rescue https://fair.tube/w/uQCVZArwr6BitUCbmTfeWJ 📢 Have a look, share the videos and help carry the ideas and discussions from #Fediday further into the Fediverse! 🚀 ❤️ And: Fediday should continue! If you would like to support #Fediday2027: https://digitalegesellschaft.de/weitermachen/ #Fediverse
🧪 @julian — SciOp & decentralised research data rescue
https://fair.tube/w/uQCVZArwr6BitUCbmTfeWJ
📢 Have a look, share the videos and help carry the ideas and discussions from #Fediday further into the Fediverse! 🚀
❤️ And: Fediday should continue!
If you would like to support #Fediday2027:
https://digitalegesellschaft.de/weitermachen/
digitalegesellschaft.de
Yep, here's my presentation at Berlin FediDay 2026, where I talked about SciOp and its federation plans. 🙂
That was a really nice environment and audience for this exchange. Good Q&A too. I'm looking forward to the recording of the “research on the fediverse” panel at the end of the session.
berlin.social
🧪 @julian@fietkau.social — SciOp & decentralised research data rescue https://fair.tube/w/uQCVZArwr6BitUCbmTfeWJ 📢 Have a look, share the videos and help carry the ideas and discussions from #Fediday further into the Fediverse! 🚀 ❤️ And: Fediday should continue! If you would like to support #Fediday2027: https://digitalegesellschaft.de/weitermachen/ #Fediverse
🧪 @julian — SciOp & decentralised research data rescue
https://fair.tube/w/uQCVZArwr6BitUCbmTfeWJ
📢 Have a look, share the videos and help carry the ideas and discussions from #Fediday further into the Fediverse! 🚀
❤️ And: Fediday should continue!
If you would like to support #Fediday2027:
https://digitalegesellschaft.de/weitermachen/
digitalegesellschaft.de
Yep, here's my presentation at Berlin FediDay 2026, where I talked about SciOp and its federation plans. 🙂
That was a really nice environment and audience for this exchange. Good Q&A too. I'm looking forward to the recording of the “research on the fediverse” panel at the end of the session.
berlin.social
🧪 @julian@fietkau.social — SciOp & decentralised research data rescue https://fair.tube/w/uQCVZArwr6BitUCbmTfeWJ 📢 Have a look, share the videos and help carry the ideas and discussions from #Fediday further into the Fediverse! 🚀 ❤️ And: Fediday should continue! If you would like to support #Fediday2027: https://digitalegesellschaft.de/weitermachen/ #Fediverse
🧪 @julian — SciOp & decentralised research data rescue
https://fair.tube/w/uQCVZArwr6BitUCbmTfeWJ
📢 Have a look, share the videos and help carry the ideas and discussions from #Fediday further into the Fediverse! 🚀
❤️ And: Fediday should continue!
If you would like to support #Fediday2027:
https://digitalegesellschaft.de/weitermachen/
digitalegesellschaft.de
Servers
- Pixelfed v0.14.0
- Ktistec v3.13.0
- XWiki ActivityPub Extension v1.7.12
- Misskey v2026.9.1
- NeoDB v0.19.3
Clients
- Fedilab v3.44.0
- Smither v3.6.7
- Pixelix v5.2.2
- RaccoonForFriendica v1.1.1
- Aria v1.5.14
Tools and Plugins
Protocol
Articles
- The Algorithm, Apparently Chronological
-----
#WeekInFediverse #Fediverse #ActivityPub
Previous edition: https://mitra.social/objects/01a0b604-9fbd-7213-b1c1-8084f3f2447b
mitra.social
Week in Fediverse 2026-09-18 - Post by @weekinfediverse
Servers
- Pixelfed v0.14.0
- Ktistec v3.13.0
- XWiki ActivityPub Extension v1.7.12
- Misskey v2026.9.1
- NeoDB v0.19.3
Clients
- Fedilab v3.44.0
- Smither v3.6.7
- Pixelix v5.2.2
- RaccoonForFriendica v1.1.1
- Aria v1.5.14
Tools and Plugins
Protocol
Articles
- The Algorithm, Apparently Chronological
-----
#WeekInFediverse #Fediverse #ActivityPub
Previous edition: https://mitra.social/objects/01a0b604-9fbd-7213-b1c1-8084f3f2447b
mitra.social
Week in Fediverse 2026-09-18 - Post by @weekinfediverse
Yep, here's my presentation at Berlin FediDay 2026, where I talked about SciOp and its federation plans. 🙂
That was a really nice environment and audience for this exchange. Good Q&A too. I'm looking forward to the recording of the “research on the fediverse” panel at the end of the session.
berlin.social
🧪 @julian@fietkau.social — SciOp & decentralised research data rescue https://fair.tube/w/uQCVZArwr6BitUCbmTfeWJ 📢 Have a look, share the videos and help carry the ideas and discussions from #Fediday further into the Fediverse! 🚀 ❤️ And: Fediday should continue! If you would like to support #Fediday2027: https://digitalegesellschaft.de/weitermachen/ #Fediverse
🧪 @julian — SciOp & decentralised research data rescue
https://fair.tube/w/uQCVZArwr6BitUCbmTfeWJ
📢 Have a look, share the videos and help carry the ideas and discussions from #Fediday further into the Fediverse! 🚀
❤️ And: Fediday should continue!
If you would like to support #Fediday2027:
https://digitalegesellschaft.de/weitermachen/
digitalegesellschaft.de
Servers
- Pixelfed v0.14.0
- Ktistec v3.13.0
- XWiki ActivityPub Extension v1.7.12
- Misskey v2026.9.1
- NeoDB v0.19.3
Clients
- Fedilab v3.44.0
- Smither v3.6.7
- Pixelix v5.2.2
- RaccoonForFriendica v1.1.1
- Aria v1.5.14
Tools and Plugins
Protocol
Articles
- The Algorithm, Apparently Chronological
-----
#WeekInFediverse #Fediverse #ActivityPub
Previous edition: https://mitra.social/objects/01a0b604-9fbd-7213-b1c1-8084f3f2447b
mitra.social
Week in Fediverse 2026-09-18 - Post by @weekinfediverse
Yep, here's my presentation at Berlin FediDay 2026, where I talked about SciOp and its federation plans. 🙂
That was a really nice environment and audience for this exchange. Good Q&A too. I'm looking forward to the recording of the “research on the fediverse” panel at the end of the session.
berlin.social
🧪 @julian@fietkau.social — SciOp & decentralised research data rescue https://fair.tube/w/uQCVZArwr6BitUCbmTfeWJ 📢 Have a look, share the videos and help carry the ideas and discussions from #Fediday further into the Fediverse! 🚀 ❤️ And: Fediday should continue! If you would like to support #Fediday2027: https://digitalegesellschaft.de/weitermachen/ #Fediverse
🧪 @julian — SciOp & decentralised research data rescue
https://fair.tube/w/uQCVZArwr6BitUCbmTfeWJ
📢 Have a look, share the videos and help carry the ideas and discussions from #Fediday further into the Fediverse! 🚀
❤️ And: Fediday should continue!
If you would like to support #Fediday2027:
https://digitalegesellschaft.de/weitermachen/
digitalegesellschaft.de
Servers
- Pixelfed v0.14.0
- Ktistec v3.13.0
- XWiki ActivityPub Extension v1.7.12
- Misskey v2026.9.1
- NeoDB v0.19.3
Clients
- Fedilab v3.44.0
- Smither v3.6.7
- Pixelix v5.2.2
- RaccoonForFriendica v1.1.1
- Aria v1.5.14
Tools and Plugins
Protocol
Articles
- The Algorithm, Apparently Chronological
-----
#WeekInFediverse #Fediverse #ActivityPub
Previous edition: https://mitra.social/objects/01a0b604-9fbd-7213-b1c1-8084f3f2447b
mitra.social
Week in Fediverse 2026-09-18 - Post by @weekinfediverse
Servers
- Pixelfed v0.14.0
- Ktistec v3.13.0
- XWiki ActivityPub Extension v1.7.12
- Misskey v2026.9.1
- NeoDB v0.19.3
Clients
- Fedilab v3.44.0
- Smither v3.6.7
- Pixelix v5.2.2
- RaccoonForFriendica v1.1.1
- Aria v1.5.14
Tools and Plugins
Protocol
Articles
- The Algorithm, Apparently Chronological
-----
#WeekInFediverse #Fediverse #ActivityPub
Previous edition: https://mitra.social/objects/01a0b604-9fbd-7213-b1c1-8084f3f2447b
mitra.social
Week in Fediverse 2026-09-18 - Post by @weekinfediverse
Servers
- Pixelfed v0.14.0
- Ktistec v3.13.0
- XWiki ActivityPub Extension v1.7.12
- Misskey v2026.9.1
- NeoDB v0.19.3
Clients
- Fedilab v3.44.0
- Smither v3.6.7
- Pixelix v5.2.2
- RaccoonForFriendica v1.1.1
- Aria v1.5.14
Tools and Plugins
Protocol
Articles
- The Algorithm, Apparently Chronological
-----
#WeekInFediverse #Fediverse #ActivityPub
Previous edition: https://mitra.social/objects/01a0b604-9fbd-7213-b1c1-8084f3f2447b
mitra.social
Week in Fediverse 2026-09-18 - Post by @weekinfediverse
How good is Fediverse security?
A while ago I asked people here whether they use Lemmy or Reddit. The results got me thinking: even among people who already use and like the Fediverse, Lemmy seems to be surprisingly small.
So I wrote about why that might be & why I think Lemmy deserves more users.
> Old forums: independent, but isolated.
> Reddit: connected, but centralized.
> Lemmy: independent and connected.
Read the whole thing here:
https://byte21516.xyz/blog/posts/2026/useLemmy/useLemmy.html
byte21516.xyz
A while ago I asked people here whether they use Lemmy or Reddit. The results got me thinking: even among people who already use and like the Fediverse, Lemmy seems to be surprisingly small.
So I wrote about why that might be & why I think Lemmy deserves more users.
> Old forums: independent, but isolated.
> Reddit: connected, but centralized.
> Lemmy: independent and connected.
Read the whole thing here:
https://byte21516.xyz/blog/posts/2026/useLemmy/useLemmy.html
byte21516.xyz
@gabboman Hey, friend. We've got some question marks about you in the #ActivityPub coverage matrix for #RFC9421. Does #wafrn accept RFC 9421 signatures? Does it send them (harder)?
swicg.github.io
Repository for a SocialCG report on how HTTP Signature is used with ActivityPub
@gabboman Hey, friend. We've got some question marks about you in the #ActivityPub coverage matrix for #RFC9421. Does #wafrn accept RFC 9421 signatures? Does it send them (harder)?
swicg.github.io
Repository for a SocialCG report on how HTTP Signature is used with ActivityPub
The event calendar should be released any moment now, but so far I can confirm that my talk proposal for #OmniOpenCon 2026 Bucharest was accepted, so I'm going to have a presentation on #ActivityPub and #ATproto!
ThreadBNC
ThreadBNC is a self-hosted social media feed aggregator/bouncer
https://youtu.be/wm3d4Hr2bP4?si=kXVdKj5UH-okn7qk
#rss #self-hosted #fediverse #activitypub #lemmy #piefed #bluesky #mastodon #archive #web-archive #docker
github.com
Threadiverse bouncer and private archive. Contribute to Dyslectric/ThreadBNC development by creating an account on GitHub.
The event calendar should be released any moment now, but so far I can confirm that my talk proposal for #OmniOpenCon 2026 Bucharest was accepted, so I'm going to have a presentation on #ActivityPub and #ATproto!
ThreadBNC
ThreadBNC is a self-hosted social media feed aggregator/bouncer
https://youtu.be/wm3d4Hr2bP4?si=kXVdKj5UH-okn7qk
#rss #self-hosted #fediverse #activitypub #lemmy #piefed #bluesky #mastodon #archive #web-archive #docker
github.com
Threadiverse bouncer and private archive. Contribute to Dyslectric/ThreadBNC development by creating an account on GitHub.
How good is Fediverse security?
I don't know how to get the eyes or ears of any big #Fediverse movers or shakers, but if someone out there who is good with code wants to jump on a golden opportunity, one is presenting itself.
A couple of days ago, Tapas warned all their creators that they were going to be shutting down. If you're not familiar with it, Tapas is a webcomics platform, and basically the only decent-sized alternative to Webtoon.
If we could throw together some sort of ActivityPub-backed Webcomics platform, we could use it as a way to get a lot more people using Fediverse-adjacent technology.
I don't have the time or energy to do any of this myself, and I am not good at coding, but I have been doing webcomics since the 1990s, and have watched the rise and fall of Keenspot, Drunk Duck, Wordpress and the RSS era, and now the modern "Walmart for Webcomics" era. This is a huge opportunity to present the Fediverse as a viable alternative.
#ActivityPub Extension 1.7.12 is now released
This release makes #XWiki able to federate with #Mastodon again: you can follow Mastodon users and send them messages from your wiki.
See https://www.xwiki.org/xwiki/bin/view/Blog/activitypub-extension-1-7-12-released/
I don't know how to get the eyes or ears of any big #Fediverse movers or shakers, but if someone out there who is good with code wants to jump on a golden opportunity, one is presenting itself.
A couple of days ago, Tapas warned all their creators that they were going to be shutting down. If you're not familiar with it, Tapas is a webcomics platform, and basically the only decent-sized alternative to Webtoon.
If we could throw together some sort of ActivityPub-backed Webcomics platform, we could use it as a way to get a lot more people using Fediverse-adjacent technology.
I don't have the time or energy to do any of this myself, and I am not good at coding, but I have been doing webcomics since the 1990s, and have watched the rise and fall of Keenspot, Drunk Duck, Wordpress and the RSS era, and now the modern "Walmart for Webcomics" era. This is a huge opportunity to present the Fediverse as a viable alternative.
#ActivityPub Extension 1.7.12 is now released
This release makes #XWiki able to federate with #Mastodon again: you can follow Mastodon users and send them messages from your wiki.
See https://www.xwiki.org/xwiki/bin/view/Blog/activitypub-extension-1-7-12-released/
„Klickwege die keinen Sinn ergeben“, @ueckueck hält nicht viel von Instagram.
Unser komplettes Gespräch über die Alternativen zu Instagram, Twitter und Co findet ihr hier: https://gultsch.video/w/kqhyU8g3ToUYYuKT3t7A4m
Oder für den Nachhauseweg in der Podcast App über diesen RSS Feed: https://gultsch.video/feeds/podcast/videos.xml?videoChannelId=1504
#Instagram #Pixelfed #Mastodon #PeerTube #Fediverse #ActivityPub #Podcast #DigitalIndependenceDay
I'm happy to say that I finally have tests for the #ActivityPub state machine of #GoActivityPub handling activities with multiple actors and multiple objects.
I'm not entirely satisfied with the way this happens as I would like to not have to reason at all about the distinction between one or more. But at least I'm happy that it works.
Hopefully I can come up with a better API in the near future.
I'm happy to say that I finally have tests for the #ActivityPub state machine of #GoActivityPub handling activities with multiple actors and multiple objects.
I'm not entirely satisfied with the way this happens as I would like to not have to reason at all about the distinction between one or more. But at least I'm happy that it works.
Hopefully I can come up with a better API in the near future.
I can't answer your question, but I can point you to the delightful fediverse clients curated list, which I maintain:
https://delightful.coding.social/delightful-fediverse-clients
delightful.coding.social
Delightful curated lists of free software, open science and information sources.
Is there a #mastodon or #activitypub mobile or web client which will show you the latest post from each account you follow? So I can more easily see the latest updates from my less frequently posting friends more than my timeline allows?
Is there a #mastodon or #activitypub mobile or web client which will show you the latest post from each account you follow? So I can more easily see the latest updates from my less frequently posting friends more than my timeline allows?
I made my plea for such a setup in the #fediverse three years ago, but since then most of the innovation in this space has happened in and around the AT protocol, which consequently pulled my attention towards that space.
However what's emerging here isn't specific to any one protocol, and #ActivityPub is neatly aligned IdP-agnostic web logins.
socialhub.activitypub.rocks
Both the primordial fediverse of ActivityPub as well as the federated Matrix have been mulling over various private-key approaches to the ideal of decentralized or nomadic identity, but I think we’ve been trying to solve too many deep-rooted problems in one go. This has kept us in a holding pattern for many years: Nomadic identity for the fediverse? Decentralised user accounts · Issue #246 · matrix-org/matrix-spec · GitHub Meanwhile there’s a major convergence of OAuth/OIDC support across apu...
#Smithereen update: fetching all replies like Mastodon does it.
Smithereen was one of the first fediverse projects to implement reply fetching, if not *the* first one, but so far it has only happened when you loaded a post using the search. Now remote comments are also re-fetched when you open a post.
Casually deciphering posts in languages I don't know is enjoyable and is good mental exercise. I'm glad it doesn't automatically translate everything.
That having been said, sometimes I'd like to publish multilingual posts where I control the translation, the same way you can publish different subtitles for a video (or use automated translation). Same for alt text.
This may be the thing that motivates me to learn something about #ActivityPub
Casually deciphering posts in languages I don't know is enjoyable and is good mental exercise. I'm glad it doesn't automatically translate everything.
That having been said, sometimes I'd like to publish multilingual posts where I control the translation, the same way you can publish different subtitles for a video (or use automated translation). Same for alt text.
This may be the thing that motivates me to learn something about #ActivityPub
Casually deciphering posts in languages I don't know is enjoyable and is good mental exercise. I'm glad it doesn't automatically translate everything.
That having been said, sometimes I'd like to publish multilingual posts where I control the translation, the same way you can publish different subtitles for a video (or use automated translation). Same for alt text.
This may be the thing that motivates me to learn something about #ActivityPub
It's fucking scary: Within less than an hour I was able to #vibecode a #Activitypub server in Rust that uses CouchDB as it's database (remember that #Nightboard83 uses PouchDB?)
I surely have to massively test and audit it over the coming days, but so far it looks very, very promising...
Casually deciphering posts in languages I don't know is enjoyable and is good mental exercise. I'm glad it doesn't automatically translate everything.
That having been said, sometimes I'd like to publish multilingual posts where I control the translation, the same way you can publish different subtitles for a video (or use automated translation). Same for alt text.
This may be the thing that motivates me to learn something about #ActivityPub
My blog's #ActivityPub powered posts have stopped showing up for me on mastodon.social and some other instances too. Does anyone have any guesses or ideas as to why?
Release v3.13.0 of Ktistec ("love who you choose to love") links the feed deck into the navigation. Feeds are now fully supported.
Feeds are custom collections of posts selected by hashtag, mention, and keyword, each grouped as terms you want any of, all of, or none of, so a feed can be as broad or as narrow as you like. The feed deck is my primary reading interface now.
The rest of the changes mostly address a variety of small federation issues:
Added
Fixed
headers signature parameter names.Changed
With feeds shipped, I'm turning my attention back to federation. There are still a ton of small issues (e.g. Likes relayed through Groups are always rejected, RFC 9421 support, blurhash support) that I'd like to squash.
epiktistes.com
The culmination of my recent work on algorithmic feeds is a #fediverse feed reading machine for all of the topics I care about. #ktistec
My blog's #ActivityPub powered posts have stopped showing up for me on mastodon.social and some other instances too. Does anyone have any guesses or ideas as to why?
Release v3.13.0 of Ktistec ("love who you choose to love") links the feed deck into the navigation. Feeds are now fully supported.
Feeds are custom collections of posts selected by hashtag, mention, and keyword, each grouped as terms you want any of, all of, or none of, so a feed can be as broad or as narrow as you like. The feed deck is my primary reading interface now.
The rest of the changes mostly address a variety of small federation issues:
Added
Fixed
headers signature parameter names.Changed
With feeds shipped, I'm turning my attention back to federation. There are still a ton of small issues (e.g. Likes relayed through Groups are always rejected, RFC 9421 support, blurhash support) that I'd like to squash.
epiktistes.com
The culmination of my recent work on algorithmic feeds is a #fediverse feed reading machine for all of the topics I care about. #ktistec
Finally set up the plumbing for adding code coverage to the #FedBOX integration test containers. 💪
Finally set up the plumbing for adding code coverage to the #FedBOX integration test containers. 💪
Contrary to what some people say, #HolosDiscover is not a scraper. It speaks #ActivityPub, and every deleted or edited post is immediately updated in the database.
It relies on the #Fediverse indexable flag, with stricter rules: accounts with #NoBot or #NoIndex in their profile, and locked accounts, are excluded. Only public posts are indexed.
I recently fixed a bug where the #NoIndex tag in bios was not respected. Sorry about that.
@Edent So, you are finding out that #ActivityPub is not very well defined? That the answer depends on every server's particular interpretation/implementation?
Unfortunately I'm finding more of the same with the #webmention protocol, which is an order of magnitude simpler than AP, but still suffers from a lack of definition in places.
@Edent So, you are finding out that #ActivityPub is not very well defined? That the answer depends on every server's particular interpretation/implementation?
Unfortunately I'm finding more of the same with the #webmention protocol, which is an order of magnitude simpler than AP, but still suffers from a lack of definition in places.
There is a Petition for the creation of the European Public Social Network:
https://epsn-initiative.eu/en/
I have casually read just that page. I have a burst of thoughts that I need to digest. Meanwhile, has someone Fediverse / ActivityPub savvy written down their thoughts about this? And is there a tag to follow?
epsn-initiative.eu
A European citizens' initiative for the first public-service social network. Join us in building a digital space under public oversight.
There is a Petition for the creation of the European Public Social Network:
https://epsn-initiative.eu/en/
I have casually read just that page. I have a burst of thoughts that I need to digest. Meanwhile, has someone Fediverse / ActivityPub savvy written down their thoughts about this? And is there a tag to follow?
epsn-initiative.eu
A European citizens' initiative for the first public-service social network. Join us in building a digital space under public oversight.
There is a Petition for the creation of the European Public Social Network:
https://epsn-initiative.eu/en/
I have casually read just that page. I have a burst of thoughts that I need to digest. Meanwhile, has someone Fediverse / ActivityPub savvy written down their thoughts about this? And is there a tag to follow?
epsn-initiative.eu
A European citizens' initiative for the first public-service social network. Join us in building a digital space under public oversight.
I was scrolling @pixelix and discovered @ghostbyte is getting the instance statistic data from new fediverse tracker they built called https://fedisea.surf
Also open source with their own crawler https://github.com/ghostbyte-dev/fedisea-web #fedidev #fediverse #ActivityPub
github.com
Website for FediSea. Contribute to ghostbyte-dev/fedisea-web development by creating an account on GitHub.
I was scrolling @pixelix and discovered @ghostbyte is getting the instance statistic data from new fediverse tracker they built called https://fedisea.surf
Also open source with their own crawler https://github.com/ghostbyte-dev/fedisea-web #fedidev #fediverse #ActivityPub
github.com
Website for FediSea. Contribute to ghostbyte-dev/fedisea-web development by creating an account on GitHub.
Contrary to what some people say, #HolosDiscover is not a scraper. It speaks #ActivityPub, and every deleted or edited post is immediately updated in the database.
It relies on the #Fediverse indexable flag, with stricter rules: accounts with #NoBot or #NoIndex in their profile, and locked accounts, are excluded. Only public posts are indexed.
I recently fixed a bug where the #NoIndex tag in bios was not respected. Sorry about that.
Currently trying to figure out if it's reasonable to have multiple "Person" actors attributed to one main "Person" actor in ActivityPub so that 1. a user can sign in as the main actor and act as the other actors tied to their account and 2. blocks and reports can work on the main actor too and affect all sub-actors.
Contrary to what some people say, #HolosDiscover is not a scraper. It speaks #ActivityPub, and every deleted or edited post is immediately updated in the database.
It relies on the #Fediverse indexable flag, with stricter rules: accounts with #NoBot or #NoIndex in their profile, and locked accounts, are excluded. Only public posts are indexed.
I recently fixed a bug where the #NoIndex tag in bios was not respected. Sorry about that.
Currently trying to figure out if it's reasonable to have multiple "Person" actors attributed to one main "Person" actor in ActivityPub so that 1. a user can sign in as the main actor and act as the other actors tied to their account and 2. blocks and reports can work on the main actor too and affect all sub-actors.
Contrary to what some people say, #HolosDiscover is not a scraper. It speaks #ActivityPub, and every deleted or edited post is immediately updated in the database.
It relies on the #Fediverse indexable flag, with stricter rules: accounts with #NoBot or #NoIndex in their profile, and locked accounts, are excluded. Only public posts are indexed.
I recently fixed a bug where the #NoIndex tag in bios was not respected. Sorry about that.
What if open protocols operate just like natural ecosystems?
In this article from Silphium Design, we explore how decentralized networks mirror ecological resilience. When nodes federate via ActivityPub, diversity prevents single-point collapse and keeps digital communities sustainable.
Curious how digital ecology maps directly to the Fediverse? Check out the latest:
https://silphiumdesign.com/digital-ecology-in-the-fediverse-decentralized/
#Fediverse #Mastodon #ActivityPub #Decentralization #WebDev #OpenSource
What if open protocols operate just like natural ecosystems?
In this article from Silphium Design, we explore how decentralized networks mirror ecological resilience. When nodes federate via ActivityPub, diversity prevents single-point collapse and keeps digital communities sustainable.
Curious how digital ecology maps directly to the Fediverse? Check out the latest:
https://silphiumdesign.com/digital-ecology-in-the-fediverse-decentralized/
#Fediverse #Mastodon #ActivityPub #Decentralization #WebDev #OpenSource
A new update to The Hitchhiker’s Guide to the Fediverse is deploying... now!
This round was shaped by useful feedback I received after sharing the project. The main point was fair: this is not a complete census of the fediverse. It is a continuously refreshed sample, discovered from deliberately chosen seed instances and their public peer relationships.
I’ve now made that limitation explicit in the documentation, rather than presenting the numbers as if they represented every ActivityPub server on the internet.
I also separated two things that are easy to blur together:
- federated services built primarily for the fediverse
- general publishing sites that participate through ActivityPub, such as WordPress or Ghost
That distinction matters when interpreting totals. A WordPress site with the ActivityPub plugin is still part of the network, but it is not the same kind of deployment as a Mastodon, Lemmy, or PeerTube instance.
social.vivaldi.net
I’ve been working on a small Fediverse project called "The Hitchhiker’s Guide to the Fediverse". It started as a simple directory of Fediverse instances, but I’ve been gradually turning it into something more useful: - discovering new instances through federation peers - filtering suspicious/spammy hosts - monitoring known instances over time - automatically hiding servers that stay offline - bringing them back if they recover - keeping the whole thing updated with GitHub Actions The monitored set is already around 27,000 instances, so I also had to rethink the collector to use bounded concurrency and checkpointing instead of checking everything one by one. Still very much a work in progress, but it’s been a fun way to learn more about how the Fediverse actually behaves in the wild. https://github.com/thanksstevenkim/the-hitchhikers-guide-to-the-fediverse #Fediverse #ActivityPub #Mastodon #OpenSource #SelfHosting
I’ve been working on a small Fediverse project called "The Hitchhiker’s Guide to the Fediverse".
It started as a simple directory of Fediverse instances, but I’ve been gradually turning it into something more useful:
- discovering new instances through federation peers
- filtering suspicious/spammy hosts
- monitoring known instances over time
- automatically hiding servers that stay offline
- bringing them back if they recover
- keeping the whole thing updated with GitHub Actions
The monitored set is already around 27,000 instances, so I also had to rethink the collector to use bounded concurrency and checkpointing instead of checking everything one by one.
Still very much a work in progress, but it’s been a fun way to learn more about how the Fediverse actually behaves in the wild.
https://github.com/thanksstevenkim/the-hitchhikers-guide-to-the-fediverse
github.com
ActivityPub 인스턴스 총 정리한 목록입니다. The Whole ActivityPub Instance List. - thanksstevenkim/the-hitchhikers-guide-to-the-fediverse
A new update to The Hitchhiker’s Guide to the Fediverse is deploying... now!
This round was shaped by useful feedback I received after sharing the project. The main point was fair: this is not a complete census of the fediverse. It is a continuously refreshed sample, discovered from deliberately chosen seed instances and their public peer relationships.
I’ve now made that limitation explicit in the documentation, rather than presenting the numbers as if they represented every ActivityPub server on the internet.
I also separated two things that are easy to blur together:
- federated services built primarily for the fediverse
- general publishing sites that participate through ActivityPub, such as WordPress or Ghost
That distinction matters when interpreting totals. A WordPress site with the ActivityPub plugin is still part of the network, but it is not the same kind of deployment as a Mastodon, Lemmy, or PeerTube instance.
social.vivaldi.net
I’ve been working on a small Fediverse project called "The Hitchhiker’s Guide to the Fediverse". It started as a simple directory of Fediverse instances, but I’ve been gradually turning it into something more useful: - discovering new instances through federation peers - filtering suspicious/spammy hosts - monitoring known instances over time - automatically hiding servers that stay offline - bringing them back if they recover - keeping the whole thing updated with GitHub Actions The monitored set is already around 27,000 instances, so I also had to rethink the collector to use bounded concurrency and checkpointing instead of checking everything one by one. Still very much a work in progress, but it’s been a fun way to learn more about how the Fediverse actually behaves in the wild. https://github.com/thanksstevenkim/the-hitchhikers-guide-to-the-fediverse #Fediverse #ActivityPub #Mastodon #OpenSource #SelfHosting
I’ve been working on a small Fediverse project called "The Hitchhiker’s Guide to the Fediverse".
It started as a simple directory of Fediverse instances, but I’ve been gradually turning it into something more useful:
- discovering new instances through federation peers
- filtering suspicious/spammy hosts
- monitoring known instances over time
- automatically hiding servers that stay offline
- bringing them back if they recover
- keeping the whole thing updated with GitHub Actions
The monitored set is already around 27,000 instances, so I also had to rethink the collector to use bounded concurrency and checkpointing instead of checking everything one by one.
Still very much a work in progress, but it’s been a fun way to learn more about how the Fediverse actually behaves in the wild.
https://github.com/thanksstevenkim/the-hitchhikers-guide-to-the-fediverse
github.com
ActivityPub 인스턴스 총 정리한 목록입니다. The Whole ActivityPub Instance List. - thanksstevenkim/the-hitchhikers-guide-to-the-fediverse
A new update to The Hitchhiker’s Guide to the Fediverse is deploying... now!
This round was shaped by useful feedback I received after sharing the project. The main point was fair: this is not a complete census of the fediverse. It is a continuously refreshed sample, discovered from deliberately chosen seed instances and their public peer relationships.
I’ve now made that limitation explicit in the documentation, rather than presenting the numbers as if they represented every ActivityPub server on the internet.
I also separated two things that are easy to blur together:
- federated services built primarily for the fediverse
- general publishing sites that participate through ActivityPub, such as WordPress or Ghost
That distinction matters when interpreting totals. A WordPress site with the ActivityPub plugin is still part of the network, but it is not the same kind of deployment as a Mastodon, Lemmy, or PeerTube instance.
social.vivaldi.net
I’ve been working on a small Fediverse project called "The Hitchhiker’s Guide to the Fediverse". It started as a simple directory of Fediverse instances, but I’ve been gradually turning it into something more useful: - discovering new instances through federation peers - filtering suspicious/spammy hosts - monitoring known instances over time - automatically hiding servers that stay offline - bringing them back if they recover - keeping the whole thing updated with GitHub Actions The monitored set is already around 27,000 instances, so I also had to rethink the collector to use bounded concurrency and checkpointing instead of checking everything one by one. Still very much a work in progress, but it’s been a fun way to learn more about how the Fediverse actually behaves in the wild. https://github.com/thanksstevenkim/the-hitchhikers-guide-to-the-fediverse #Fediverse #ActivityPub #Mastodon #OpenSource #SelfHosting
I’ve been working on a small Fediverse project called "The Hitchhiker’s Guide to the Fediverse".
It started as a simple directory of Fediverse instances, but I’ve been gradually turning it into something more useful:
- discovering new instances through federation peers
- filtering suspicious/spammy hosts
- monitoring known instances over time
- automatically hiding servers that stay offline
- bringing them back if they recover
- keeping the whole thing updated with GitHub Actions
The monitored set is already around 27,000 instances, so I also had to rethink the collector to use bounded concurrency and checkpointing instead of checking everything one by one.
Still very much a work in progress, but it’s been a fun way to learn more about how the Fediverse actually behaves in the wild.
https://github.com/thanksstevenkim/the-hitchhikers-guide-to-the-fediverse
github.com
ActivityPub 인스턴스 총 정리한 목록입니다. The Whole ActivityPub Instance List. - thanksstevenkim/the-hitchhikers-guide-to-the-fediverse
Servers
- Wafrn v2026.09.11
- TinyAP v0.2.0
- Mastodon v4.7.2
- PeerTube v8.3.0
- NodeBB v4.16.0
- NeoDB v0.19.0
- Gush! v0.0.42
- PieFed v1.7.16
- Lemmy Dev Update August 2026 and 1.0.0-beta.2
- Trunk & Tidbits, August 2026 (Mastodon)
Clients
- Fedilab v3.43.4
- Smither v3.6.5
- Voyager v2.49.1
- mlmym v26.4.0
Protocol
- FEP-22cd: Attributing translations
- FEP-7628: Move actor (Final)
Articles
- People, Algorithms, Social media and the Fediverse
-----
#WeekInFediverse #Fediverse #ActivityPub
Previous edition: https://mitra.social/objects/01a091f1-ecbc-7923-ad6a-e9bd3bae78e3
mitra.social
Week in Fediverse 2026-09-11 - Post by @weekinfediverse
Servers
- Wafrn v2026.09.11
- TinyAP v0.2.0
- Mastodon v4.7.2
- PeerTube v8.3.0
- NodeBB v4.16.0
- NeoDB v0.19.0
- Gush! v0.0.42
- PieFed v1.7.16
- Lemmy Dev Update August 2026 and 1.0.0-beta.2
- Trunk & Tidbits, August 2026 (Mastodon)
Clients
- Fedilab v3.43.4
- Smither v3.6.5
- Voyager v2.49.1
- mlmym v26.4.0
Protocol
- FEP-22cd: Attributing translations
- FEP-7628: Move actor (Final)
Articles
- People, Algorithms, Social media and the Fediverse
-----
#WeekInFediverse #Fediverse #ActivityPub
Previous edition: https://mitra.social/objects/01a091f1-ecbc-7923-ad6a-e9bd3bae78e3
mitra.social
Week in Fediverse 2026-09-11 - Post by @weekinfediverse
@smallcircles@social.coop but I agree that some do get confused by assuming that #Mastodon (a software), #Fediverse (an ecosystem), #ActivityPub (a protocol) are synonyms and some pedagogy is needed, especially to journalists who write about this. And especially if they are making comparison with other things that are not comparable (e.g. #Bluesky)
bonfire.cim.warwick.ac.uk
This is a federated digital space running [bonfire](https://bonfirenetworks.org/), a...
@smallcircles@social.coop but I agree that some do get confused by assuming that #Mastodon (a software), #Fediverse (an ecosystem), #ActivityPub (a protocol) are synonyms and some pedagogy is needed, especially to journalists who write about this. And especially if they are making comparison with other things that are not comparable (e.g. #Bluesky)
bonfire.cim.warwick.ac.uk
This is a federated digital space running [bonfire](https://bonfirenetworks.org/), a...
I'm going to preface this with that I have no intention of doing this at all, this is just a hypothetical that has been nagging me.
Let's say I were to delete this instance tomorrow, or maybe the domain expires and I never renew it... Does the cache of my posts and info and such eventually disappear from other fediverse instances? Do posts have a TTL that gets refreshed with the originating server or anything like that? I am sure that it is different across different implementations, so I'm really just looking for a general answer...
I'm going to preface this with that I have no intention of doing this at all, this is just a hypothetical that has been nagging me.
Let's say I were to delete this instance tomorrow, or maybe the domain expires and I never renew it... Does the cache of my posts and info and such eventually disappear from other fediverse instances? Do posts have a TTL that gets refreshed with the originating server or anything like that? I am sure that it is different across different implementations, so I'm really just looking for a general answer...
FEP-8fcf aka followers collection synchronization has been shipped in 🥳
https://github.com/pixelfed/pixelfed/commit/dedfa67b708f3f4ea73aff92889cf1bb95a44329
Sender: followers-only Create deliveries carry a signed Collection-Synchronization header scoped to the authority of each inbox, and the partial followers collection is served to authenticated inst...
FEP-8fcf aka followers collection synchronization has been shipped in 🥳
https://github.com/pixelfed/pixelfed/commit/dedfa67b708f3f4ea73aff92889cf1bb95a44329
Sender: followers-only Create deliveries carry a signed Collection-Synchronization header scoped to the authority of each inbox, and the partial followers collection is served to authenticated inst...
@evan thank you!
Yes, found Unhosted via the @nlnet projects page. Was unclear on the state of the project (everything is undated), but underlying concepts certainly still hold sway.
Indeed I think the layering makes sense, is on the right track. Remember the persona's I discerned earlier? Where the Protocol developer create comprehensive specs for the Protocol implementer, who can subsequently offer #ActivityPub implementations that allow the Solution developer to get to fully focus on their application or business domain, shielded from technical intricacies and wire-level plumbing.
A robust Protocol layer will enable the network effects, but unlocking the virtuous cycle imho requires tackling more social challenges. It is an enabler to go beyond the app-centric fedi, but with post-facto #interoperability the accepted work method, will not solve the anti-patterns mentioned in the article.
The social challenge is facilitating inclusive solution development at ecosystem-wide levels.
@Marloezovic interessant!
Een kleine nitpick over de hashtags. Waarom aan fediverse zijde alleen Mastodon noemen, 1 enkele app van velen, tegenover het communicatie protocol van de atmosphere?
Equivalenten:
- fediverse / atmosphere
- ActivityPub / ATProto
- Mastodon / Bluesky
Ik onderhou de #ActivityPub app lijst en alle entries behalve één vallen zo buiten de boot :)
https://delightful.coding.social/delightful-fediverse-experience
delightful.coding.social
Delightful curated lists of free software, open science and information sources.
Anyone have any recommendations for a mastodon (or activitypub) server that allows longer posts, say 3k or longer? Also looking for posting tech based content.
Anyone have any recommendations for a mastodon (or activitypub) server that allows longer posts, say 3k or longer? Also looking for posting tech based content.
Opt-out happens where random fedi app developers are your landlords who decide what services you get, and who were still benevolent enough to give you a choice (if you happen to find out about these services).
Opt-in fits a public online space where you have your own home and self-sovereignty to decide the services you want to consume in your social household. It is where you have control of your own life.
https://coding.social/blog/prosocial-web
#SX #ProsocialWeb #ActivityPub #fediverse #consent #sovereignty #DigitAlautonomy

coding.social
Only humans make the social web social.
@smallcircles I think at our last meeting, we suggested the following layered architecture for #ActivityPub API work:
- A base profile; just enough to get things working
- A bunch of discoverable additional features, like SSE and search
- An "advanced" profile, essentially base profile + selected features
@evan thank you!
Yes, found Unhosted via the @nlnet projects page. Was unclear on the state of the project (everything is undated), but underlying concepts certainly still hold sway.
Indeed I think the layering makes sense, is on the right track. Remember the persona's I discerned earlier? Where the Protocol developer create comprehensive specs for the Protocol implementer, who can subsequently offer #ActivityPub implementations that allow the Solution developer to get to fully focus on their application or business domain, shielded from technical intricacies and wire-level plumbing.
A robust Protocol layer will enable the network effects, but unlocking the virtuous cycle imho requires tackling more social challenges. It is an enabler to go beyond the app-centric fedi, but with post-facto #interoperability the accepted work method, will not solve the anti-patterns mentioned in the article.
The social challenge is facilitating inclusive solution development at ecosystem-wide levels.
@smallcircles I think at our last meeting, we suggested the following layered architecture for #ActivityPub API work:
- A base profile; just enough to get things working
- A bunch of discoverable additional features, like SSE and search
- An "advanced" profile, essentially base profile + selected features
So, what is the #Fediverse scene like in #Dublin #Ireland? The #w3c has the annual #TPAC meeting in October. There will be #ActivityPub folks in Dublin for TPAC, and it would be cool to do a #meetup at a pub or hackerspace.
laravel-activitypub, a batteries included ActivityPub package for laravel.
Soon will power @pixelfed and @joinloops
Shoutout to @FediverseSymbol
laravel-activitypub, a batteries included ActivityPub package for laravel.
Soon will power @pixelfed and @joinloops
Shoutout to @FediverseSymbol
So, what is the #Fediverse scene like in #Dublin #Ireland? The #w3c has the annual #TPAC meeting in October. There will be #ActivityPub folks in Dublin for TPAC, and it would be cool to do a #meetup at a pub or hackerspace.
Opt-out happens where random fedi app developers are your landlords who decide what services you get, and who were still benevolent enough to give you a choice (if you happen to find out about these services).
Opt-in fits a public online space where you have your own home and self-sovereignty to decide the services you want to consume in your social household. It is where you have control of your own life.
https://coding.social/blog/prosocial-web
#SX #ProsocialWeb #ActivityPub #fediverse #consent #sovereignty #DigitAlautonomy

coding.social
Only humans make the social web social.
So, what is the #Fediverse scene like in #Dublin #Ireland? The #w3c has the annual #TPAC meeting in October. There will be #ActivityPub folks in Dublin for TPAC, and it would be cool to do a #meetup at a pub or hackerspace.
Ok, people of #Mastodon, can anyone explain to me why it is not possible to somehow connect #ActivityPub and #ATProto. It seems a bit ridiculous to me that we have #alternatives to #BigTech that seem to work against each other.
Links to posts or blogs that explain the problem a bit further would also be super helpful. Please share! Thank you! #Fediverse #Bluesky #Eurosky
Just finished listening to this interview with Eli Mallen (@iameli) of https://stream.place (from last year) talk about taking on Twitch in the open social web. He talked about his history in other protocol spaces like SSB and Ethereum and why he chose to work with #ATProto and not #ActivityPub
https://devtools.fm/episode/140
devtools.fm
Eli Mallon shares how Streamplace is building the future of decentralized video streaming on atproto, challenging traditional platforms like Twitch.
Laravel + PHP gains a batteries included package for ActivityPub support, with both draft-cavage and RFC 9421 HTTP Sig support and the ability to extend every aspect, with before() and after() hooks for easy custom processing.
It's quite amazing.
I'll be tagging a new release this week.
laravel-activitypub, a batteries included ActivityPub package for laravel.
Soon will power @pixelfed and @joinloops
Shoutout to @FediverseSymbol
The social web is evolving: into open, decentralized spaces no single organization controls. The Social Web Foundation is building a global town square that works for everyone: E2E encryption, GDPR-ready privacy, a healthier fediverse.
laravel-activitypub, a batteries included ActivityPub package for laravel.
Soon will power @pixelfed and @joinloops
Shoutout to @FediverseSymbol
Okay, laravel-activitypub is near ready to ship!
I'll be testing it with shutter, a single user activitypub server for image sharing.
Making real progress on laravel-activitypub, a batteries included ActivityPub package for laravel.
This will replace federation logic in @pixelfed and @joinloops and make it really easy to implement AP in your own laravel apps!
Making real progress on laravel-activitypub, a batteries included ActivityPub package for laravel.
This will replace federation logic in @pixelfed and @joinloops and make it really easy to implement AP in your own laravel apps!
Making real progress on laravel-activitypub, a batteries included ActivityPub package for laravel.
This will replace federation logic in @pixelfed and @joinloops and make it really easy to implement AP in your own laravel apps!
Ist das ein Thema, was wir auch mal bei Gelegenheit bei einem fedimainz Treffen diskutieren sollten? Die ActivityPub API wurde ja, wenn ich mich nicht täusche, durch @sl007 erwähnt...
Was ist der "Paradigm Shift" genau, der im Post gemeint ist? Brauchen wir den tatsächlich? Ist eine ehrlich gemeinte Frage... (Ich glaube, dringender ist es erstmal, zuzusehen, dass wir in absehbarer Zeit überhaupt ein nächstes Treffen hinbekommen, aber ich wollte auch einfach weiter Themen sammeln.)
social.coop
#Fediverse today consists of apps and app platforms. It is entirely app-centric. Since fediverse (luckily) didn't attract much corporate attention yet, it is dominated by #FOSS culture, where people on the #SocialWeb are all "users" that are dependent on FOSS developers to serve their needs. Every dev focuses on their app features first and #interoperability second, leading to a patchwork of poorly integrated apps, and a janky overall experience. The app delivery model is not much different than Web 2.0. A paradigm shift is possible, to a #SocialNetwork of apps and services, a service-oriented #fedi. The efforts around the #ActivityPub API can be the enabler of this shift. In this new paradigm devs introduce solutions to a unified #SocialNetworking space, delivering new services to participants on the network. On top of a robust Protocol Layer they model social use cases in the Data Layer, integrating with services and building blocks already available. https://coding.social/blog/prosocial-web
#Fediverse today consists of apps and app platforms. It is entirely app-centric.
Since fediverse (luckily) didn't attract much corporate attention yet, it is dominated by #FOSS culture, where people on the #SocialWeb are all "users" that are dependent on FOSS developers to serve their needs. Every dev focuses on their app features first and #interoperability second, leading to a patchwork of poorly integrated apps, and a janky overall experience. The app delivery model is not much different than Web 2.0.
A paradigm shift is possible, to a #SocialNetwork of apps and services, a service-oriented #fedi. The efforts around the #ActivityPub API can be the enabler of this shift.
In this new paradigm devs introduce solutions to a unified #SocialNetworking space, delivering new services to participants on the network. On top of a robust Protocol Layer they model social use cases in the Data Layer, integrating with services and building blocks already available.

coding.social
Only humans make the social web social.
Whatever #Mastodon and #ActivityPub are now, it was an #experiment and proof that we can establish a #system outside the existing one—one that arose entirely from #collaboration and #community. That is #anarchy in action—proof that we can #organize ourselves on a larger scale, and that we should. That is the only hope, to be precise. We have the power; that has been proven. Let’s do it. Let’s take over the world. #oss #foss #fight #anarchist #fedi #fediverse #federation
mastodon.social
Attached: 1 image
Whatever #Mastodon and #ActivityPub are now, it was an #experiment and proof that we can establish a #system outside the existing one—one that arose entirely from #collaboration and #community. That is #anarchy in action—proof that we can #organize ourselves on a larger scale, and that we should. That is the only hope, to be precise. We have the power; that has been proven. Let’s do it. Let’s take over the world. #oss #foss #fight #anarchist #fedi #fediverse #federation
mastodon.social
Attached: 1 image
every so often i crank inbox logging up as loud as it will go and then spend the next two weeks fixing federation bugs. this is apparently the start of those two weeks.
fixing issues with http signatures now...
Trees, Pixels & Press: The Human Touch in Design - Prisca Schmarsow - E98
Special thank you to @humanum@mastodon.art
🚀 #VOD tubefree.org/w/891jDRNf6zVg9...
Never miss an episode!
🔎 #Follow @ozoned@tubefree.org
📡 #RSS tubefree.org/feeds/videos.xml?videoChannelId=5934
🎧 #Podcast tubefree.org/feeds/podcast/videos.xml?videoChannelId=5934 #FiresideFedi #FiresideChat #Podcast #VideoPodcast #Fediverse #Fedi #ActivityPub #DecentralizedSocial #OpenSocialWeb #Community #VodCast
btfree.social
BT Free is a non-profit organization founded by @ozoned@btfree.social . It's goal is for digital privacy rights, advocacy and consulting. This goal will be attained by hosting open platforms to allow
every so often i crank inbox logging up as loud as it will go and then spend the next two weeks fixing federation bugs. this is apparently the start of those two weeks.
fixing issues with http signatures now...
The vocabulary part of #GoActivityPub is getting closer and closer to being able of generating and operating with custom #ActivityPub data types.
https://go-activitypub.federated.id/lib/rfc/code-generation.html
go-activitypub.federated.id
The vocabulary part of #GoActivityPub is getting closer and closer to being able of generating and operating with custom #ActivityPub data types.
https://go-activitypub.federated.id/lib/rfc/code-generation.html
go-activitypub.federated.id
The vocabulary part of #GoActivityPub is getting closer and closer to being able of generating and operating with custom #ActivityPub data types.
https://go-activitypub.federated.id/lib/rfc/code-generation.html
go-activitypub.federated.id
LOLA is a proposal for live online account portability between two ActivityPub servers at the request of a user. The goal is to allow the user to pursue the following workflow:
- Request a destination server to copy an ActivityPub account from a source server
- Authorize the destination server to the source server
- See the content in its new location after the destination server completes copying it over
- Optionally, at a later time, ask the source server to send notifications to followers that the account is moving
- Optionally, at a later time, redirect the content at the source server to the destination server
swicg.github.io
LOLA is a proposal for live online account portability between two ActivityPub servers at the request of a user. The goal is to allow the user to pursue the following workflow:
- Request a destination server to copy an ActivityPub account from a source server
- Authorize the destination server to the source server
- See the content in its new location after the destination server completes copying it over
- Optionally, at a later time, ask the source server to send notifications to followers that the account is moving
- Optionally, at a later time, redirect the content at the source server to the destination server
swicg.github.io
LOLA is a proposal for live online account portability between two ActivityPub servers at the request of a user. The goal is to allow the user to pursue the following workflow:
- Request a destination server to copy an ActivityPub account from a source server
- Authorize the destination server to the source server
- See the content in its new location after the destination server completes copying it over
- Optionally, at a later time, ask the source server to send notifications to followers that the account is moving
- Optionally, at a later time, redirect the content at the source server to the destination server
swicg.github.io
LOLA is a proposal for live online account portability between two ActivityPub servers at the request of a user. The goal is to allow the user to pursue the following workflow:
- Request a destination server to copy an ActivityPub account from a source server
- Authorize the destination server to the source server
- See the content in its new location after the destination server completes copying it over
- Optionally, at a later time, ask the source server to send notifications to followers that the account is moving
- Optionally, at a later time, redirect the content at the source server to the destination server
swicg.github.io
🆕 blog! “ActivityBot is the recipient of an NLnet grant!”
Back in February, I applied for NLnet's Next Generation Zero grant. They were looking for Fediverse projects to help rewild the social media landscape. Or, as they describe it:
Reclaim the public nature of the internet
Small and medium-sized R&D grants between 5.000 and 50.000 euro,…
👀 Read more: https://shkspr.mobi/blog/2026/08/activitybot-is-the-recipient-of-an-nlnet-grant/
⸻
#ActivityBot #ActivityPub #fediverse #NLnet
shkspr.mobi
Back in February, I applied for NLnet's Next Generation Zero grant. They were looking for Fediverse projects to help rewild the social media landscape. Or, as they describe it: Reclaim the public nature of the internet Small and medium-sized R&D grants between 5.000 and 50.000 euro, with the possibility to scale up. I run ActivityBot - it is a single-file ActivityPub server suitable for…
Right after @berlinfediday is over, I am hosting another edition of
Berlin Federated Network Exploration Circle / #BFNEC
Sept 13th at 7pm at @offline
✸ @pukkamustard will talk about their work on ERIS and give us the backstory on their initiation of the fediverse's FEP process.
⁂ @f will discuss his work on @sutty, a platform aimed at giving organizations and collectives the means to publish and host more secure websites. He will also talk about how they recently added #ActivityPub federation.

Berlin Federated Network Exploration Circle a cosy meeting of humans who want to discuss this strange networked world together September 13th, 2026 (7pm) at Offline ✸ pukkamustard will talk about their work on ERIS and give us the backstory on their initiation of the fediverse's FEP process. ❋ f will discuss his work on Sutty, a platform aimed at giving organizations and collectives the means to publish and host more secure websites. He will also talk about how they recently added ActivityPub federation. ⁂
#nobot is good enough... until it is de-facto #standard and there's no way back from #protocol decay, that is now common practice across the installed base.
That an actor has a profile specified in a particular way, and that that profile has a bio description, and that that bio description may contain a growing number of special control words. A growing collection of #domain-specific language that arbitrary #apps impose, is severe example of protocol decay that significantly decreases the attractiveness and value of adopting #ActivityPub. To adopt, you are required to hem yourself into very particular approaches, accepting all kinds of app-specific leaky abstractions that may be totally irrelevant to the business or application domain your project models solutions for.
On app-centric #fediverse-we-have an app developer has to force their app into a pretzel for interoperable wire exchange that doesn't look and feel like #microblogging.
This will only grow worse over time.
Today at 1430 I'll host a barcamp related to payments on the Fediverse – how could we transfer money to get away from the advertising model of monetization? How to pay for things and services? Can we put this into Activitypub somehow?
This is going to be a technical brainstorming session.
Nice. Good luck with the barcamp!
> Can we put this into Activitypub somehow?
Should we put this into #ActivityPub, is another relevant question.
> This is going to be a technical brainstorming session
A question that is more in the social realms: What will be the impact on the social dynamics and culture if (micro)payment becomes native to the protocol?
Today at 1430 I'll host a barcamp related to payments on the Fediverse – how could we transfer money to get away from the advertising model of monetization? How to pay for things and services? Can we put this into Activitypub somehow?
This is going to be a technical brainstorming session.
Is #Pleroma good?
I need to admin a small social media instance for say 10 non-techie people so we can publish photos and text posts. Just for outreach and reminders and event flyers
Mastodon looks heavier and I want something #ActivityPub so it can syndicate across the #Fediverse and bridge to BlueSky (I don't want to run a BS directly if I can help it)
🆕 blog! “ActivityPub - How to send an updated user profile to Mastodon and the Fediverse”
Let's suppose you've updated the description of your ActivityPub account from "World's Number 1 Taylor Swift Fan" to "This account is now a Nickleback Truther". How do you let the rest of the Fediverse know that you've…
👀 Read more: https://shkspr.mobi/blog/2026/09/activitypub-how-to-send-an-updated-user-profile-to-mastodon-and-the-fediverse/
⸻
#ActivityBot #ActivityPub #fediverse #mastodon
shkspr.mobi
Let's suppose you've updated the description of your ActivityPub account from "World's Number 1 Taylor Swift Fan" to "This account is now a Nickleback Truther". How do you let the rest of the Fediverse know that you've changed your allegiance? By default, most Mastodon instances won't periodically poll your account information just to see if you've updated it. So how does the information get…
Anyone who is interested in the #Fediverse and #ActivityPub is welcome to join the Social Web Community Group #SocialCG at the #w3c.
It's the group that develops and advances the social standards that make this network go. It's open to anyone. If you have specific interests, we have task forces that work on specific topics. Or you can start one!
It's easy to join.
w3.org
🆕 blog! “ActivityPub - How to send an updated user profile to Mastodon and the Fediverse”
Let's suppose you've updated the description of your ActivityPub account from "World's Number 1 Taylor Swift Fan" to "This account is now a Nickleback Truther". How do you let the rest of the Fediverse know that you've…
👀 Read more: https://shkspr.mobi/blog/2026/09/activitypub-how-to-send-an-updated-user-profile-to-mastodon-and-the-fediverse/
⸻
#ActivityBot #ActivityPub #fediverse #mastodon
shkspr.mobi
Let's suppose you've updated the description of your ActivityPub account from "World's Number 1 Taylor Swift Fan" to "This account is now a Nickleback Truther". How do you let the rest of the Fediverse know that you've changed your allegiance? By default, most Mastodon instances won't periodically poll your account information just to see if you've updated it. So how does the information get…
🆕 blog! “ActivityPub - How to send an updated user profile to Mastodon and the Fediverse”
Let's suppose you've updated the description of your ActivityPub account from "World's Number 1 Taylor Swift Fan" to "This account is now a Nickleback Truther". How do you let the rest of the Fediverse know that you've…
👀 Read more: https://shkspr.mobi/blog/2026/09/activitypub-how-to-send-an-updated-user-profile-to-mastodon-and-the-fediverse/
⸻
#ActivityBot #ActivityPub #fediverse #mastodon
shkspr.mobi
Let's suppose you've updated the description of your ActivityPub account from "World's Number 1 Taylor Swift Fan" to "This account is now a Nickleback Truther". How do you let the rest of the Fediverse know that you've changed your allegiance? By default, most Mastodon instances won't periodically poll your account information just to see if you've updated it. So how does the information get…
Sharkey is generally no longer actively maintained. Much like IceShrimp, which shifted its focus from IceShrimp-js to IceShrimp.net, Sharkey is effectively in "maintenance mode only." This means it still receives critical security updates and required patches, but it generally no longer tracks upstream Misskey development.
Sharkey's latest release: 2025.4.7
Misskey's latest release: 2026.9.0
I had to laugh when, back on January 13, 2026, they titled a release "2025.4.5 — We'll catch up, we promise." The release right before that had been 6 months earlier. In all the time since, they've only pushed out 2 security updates (2025.4.6 and 2025.4.7). They are now well over a year behind Misskey's development.
If anyone has the time and skills, I hope someone builds a script to migrate Sharkey sites back to Misskey. Since Sharkey is a fork of Misskey (as was FireFish), it should be possible and is a smart precaution.
#Sharkey #Misskey #FireFish #OpenSource #Fediverse #ActivityPub
Servers
- Hubzilla v11.4.1
- Bookwyrm v0.9.3
- Cookifed v0.1.0
- Ktistec v3.12.1
- Misskey v2026.9.0
- gathio v1.6.7
- NeoDB v0.18.2
- Appy v0.7.0
- PieFed v1.7.15
- Wafrn v2026.09.01
Clients
- Voyager v2.49.0
- Miria v4.0.1
- Aria v1.5.13
Tools and Plugins
- Enable Mastodon Apps v1.6.4 (WordPress plugin)
Articles
- Static ActivityPub Publishing
-----
#WeekInFediverse #Fediverse #ActivityPub
Previous edition: https://mitra.social/objects/01a06eb0-8683-70d3-bb1d-19ef2967e02b
mitra.social
Week in Fediverse 2026-09-04 - Post by @weekinfediverse
Servers
- Hubzilla v11.4.1
- Bookwyrm v0.9.3
- Cookifed v0.1.0
- Ktistec v3.12.1
- Misskey v2026.9.0
- gathio v1.6.7
- NeoDB v0.18.2
- Appy v0.7.0
- PieFed v1.7.15
- Wafrn v2026.09.01
Clients
- Voyager v2.49.0
- Miria v4.0.1
- Aria v1.5.13
Tools and Plugins
- Enable Mastodon Apps v1.6.4 (WordPress plugin)
Articles
- Static ActivityPub Publishing
-----
#WeekInFediverse #Fediverse #ActivityPub
Previous edition: https://mitra.social/objects/01a06eb0-8683-70d3-bb1d-19ef2967e02b
mitra.social
Week in Fediverse 2026-09-04 - Post by @weekinfediverse
Servers
- Hubzilla v11.4.1
- Bookwyrm v0.9.3
- Cookifed v0.1.0
- Ktistec v3.12.1
- Misskey v2026.9.0
- gathio v1.6.7
- NeoDB v0.18.2
- Appy v0.7.0
- PieFed v1.7.15
- Wafrn v2026.09.01
Clients
- Voyager v2.49.0
- Miria v4.0.1
- Aria v1.5.13
Tools and Plugins
- Enable Mastodon Apps v1.6.4 (WordPress plugin)
Articles
- Static ActivityPub Publishing
-----
#WeekInFediverse #Fediverse #ActivityPub
Previous edition: https://mitra.social/objects/01a06eb0-8683-70d3-bb1d-19ef2967e02b
mitra.social
Week in Fediverse 2026-09-04 - Post by @weekinfediverse
Anyone who is interested in the #Fediverse and #ActivityPub is welcome to join the Social Web Community Group #SocialCG at the #w3c.
It's the group that develops and advances the social standards that make this network go. It's open to anyone. If you have specific interests, we have task forces that work on specific topics. Or you can start one!
It's easy to join.
w3.org
Anyone who is interested in the #Fediverse and #ActivityPub is welcome to join the Social Web Community Group #SocialCG at the #w3c.
It's the group that develops and advances the social standards that make this network go. It's open to anyone. If you have specific interests, we have task forces that work on specific topics. Or you can start one!
It's easy to join.
w3.org
Anyone who is interested in the #Fediverse and #ActivityPub is welcome to join the Social Web Community Group #SocialCG at the #w3c.
It's the group that develops and advances the social standards that make this network go. It's open to anyone. If you have specific interests, we have task forces that work on specific topics. Or you can start one!
It's easy to join.
w3.org
I swore not to speak about #FediHood any more, but what #Pixelfed is doing is great news for #ActivityPub.
Pixelfed already sends the location as a Place object with latitude and longitude, so FediHood can read it and put markers on a map.
Local timelines used to mean one instance. Now they can mean people actually around you.
mastodon.social
Attached: 1 image Places, a unique Pixelfed feature that allows you to add an optional location to your posts to make them discoverable by place! We are working to support custom places too! #pixelfed
Sharkey is generally no longer actively maintained. Much like IceShrimp, which shifted its focus from IceShrimp-js to IceShrimp.net, Sharkey is effectively in "maintenance mode only." This means it still receives critical security updates and required patches, but it generally no longer tracks upstream Misskey development.
Sharkey's latest release: 2025.4.7
Misskey's latest release: 2026.9.0
I had to laugh when, back on January 13, 2026, they titled a release "2025.4.5 — We'll catch up, we promise." The release right before that had been 6 months earlier. In all the time since, they've only pushed out 2 security updates (2025.4.6 and 2025.4.7). They are now well over a year behind Misskey's development.
If anyone has the time and skills, I hope someone builds a script to migrate Sharkey sites back to Misskey. Since Sharkey is a fork of Misskey (as was FireFish), it should be possible and is a smart precaution.
#Sharkey #Misskey #FireFish #OpenSource #Fediverse #ActivityPub
Servers
- Hubzilla v11.4.1
- Bookwyrm v0.9.3
- Cookifed v0.1.0
- Ktistec v3.12.1
- Misskey v2026.9.0
- gathio v1.6.7
- NeoDB v0.18.2
- Appy v0.7.0
- PieFed v1.7.15
- Wafrn v2026.09.01
Clients
- Voyager v2.49.0
- Miria v4.0.1
- Aria v1.5.13
Tools and Plugins
- Enable Mastodon Apps v1.6.4 (WordPress plugin)
Articles
- Static ActivityPub Publishing
-----
#WeekInFediverse #Fediverse #ActivityPub
Previous edition: https://mitra.social/objects/01a06eb0-8683-70d3-bb1d-19ef2967e02b
mitra.social
Week in Fediverse 2026-09-04 - Post by @weekinfediverse
I swore not to speak about #FediHood any more, but what #Pixelfed is doing is great news for #ActivityPub.
Pixelfed already sends the location as a Place object with latitude and longitude, so FediHood can read it and put markers on a map.
Local timelines used to mean one instance. Now they can mean people actually around you.
mastodon.social
Attached: 1 image Places, a unique Pixelfed feature that allows you to add an optional location to your posts to make them discoverable by place! We are working to support custom places too! #pixelfed
I swore not to speak about #FediHood any more, but what #Pixelfed is doing is great news for #ActivityPub.
Pixelfed already sends the location as a Place object with latitude and longitude, so FediHood can read it and put markers on a map.
Local timelines used to mean one instance. Now they can mean people actually around you.
mastodon.social
Attached: 1 image Places, a unique Pixelfed feature that allows you to add an optional location to your posts to make them discoverable by place! We are working to support custom places too! #pixelfed
I swore not to speak about #FediHood any more, but what #Pixelfed is doing is great news for #ActivityPub.
Pixelfed already sends the location as a Place object with latitude and longitude, so FediHood can read it and put markers on a map.
Local timelines used to mean one instance. Now they can mean people actually around you.
mastodon.social
Attached: 1 image Places, a unique Pixelfed feature that allows you to add an optional location to your posts to make them discoverable by place! We are working to support custom places too! #pixelfed
Servers
- Hubzilla v11.4.1
- Bookwyrm v0.9.3
- Cookifed v0.1.0
- Ktistec v3.12.1
- Misskey v2026.9.0
- gathio v1.6.7
- NeoDB v0.18.2
- Appy v0.7.0
- PieFed v1.7.15
- Wafrn v2026.09.01
Clients
- Voyager v2.49.0
- Miria v4.0.1
- Aria v1.5.13
Tools and Plugins
- Enable Mastodon Apps v1.6.4 (WordPress plugin)
Articles
- Static ActivityPub Publishing
-----
#WeekInFediverse #Fediverse #ActivityPub
Previous edition: https://mitra.social/objects/01a06eb0-8683-70d3-bb1d-19ef2967e02b
mitra.social
Week in Fediverse 2026-09-04 - Post by @weekinfediverse
For the backend I was told that Rust and Go are way faster and safer than Java/Node/Python, since Go is easier to implement, write and has a more complete #activitypub implementation, I went with that.
For the frontend, I worked with php, js and next in the past and I end up choosing Next because it's faster to design and change the design, also I end up hating php and pure js XD
I'm copying other fedi projects docker, redis, postgres stack so I could get started faster.
Sharkey is generally no longer actively maintained. Much like IceShrimp, which shifted its focus from IceShrimp-js to IceShrimp.net, Sharkey is effectively in "maintenance mode only." This means it still receives critical security updates and required patches, but it generally no longer tracks upstream Misskey development.
Sharkey's latest release: 2025.4.7
Misskey's latest release: 2026.9.0
I had to laugh when, back on January 13, 2026, they titled a release "2025.4.5 — We'll catch up, we promise." The release right before that had been 6 months earlier. In all the time since, they've only pushed out 2 security updates (2025.4.6 and 2025.4.7). They are now well over a year behind Misskey's development.
If anyone has the time and skills, I hope someone builds a script to migrate Sharkey sites back to Misskey. Since Sharkey is a fork of Misskey (as was FireFish), it should be possible and is a smart precaution.
#Sharkey #Misskey #FireFish #OpenSource #Fediverse #ActivityPub
I know exactly who needs to hear this but it's best put as a reminder to everyone that fake accounts exist, that they are actively being used, and that they will be used against you and/or your friends
If you have a significant following, please try to be careful not to legitimize these accounts by following them
The #fediverse, a network of independent services, offers an #alternative to centralised platforms by enabling #usercontrol over data and fostering localised online communities. Join #EFF’s Babette Ngene and Rory Mir, along with Bruce Schneier and Mallory Knodel, for a discussion on building a #decentralisedweb that prioritises #userchoice and #datacontrol. https://join.eff.org/Additional-donation/?Fedizen.EU #Fedizen #Fediverse #ActivityPub #News
join.eff.org
The #fediverse, a network of independent services, offers an #alternative to centralised platforms by enabling #usercontrol over data and fostering localised online communities. Join #EFF’s Babette Ngene and Rory Mir, along with Bruce Schneier and Mallory Knodel, for a discussion on building a #decentralisedweb that prioritises #userchoice and #datacontrol. https://join.eff.org/Additional-donation/?Fedizen.EU #Fedizen #Fediverse #ActivityPub #News
join.eff.org
I know exactly who needs to hear this but it's best put as a reminder to everyone that fake accounts exist, that they are actively being used, and that they will be used against you and/or your friends
If you have a significant following, please try to be careful not to legitimize these accounts by following them
Anyone who is interested in the #Fediverse and #ActivityPub is welcome to join the Social Web Community Group #SocialCG at the #w3c.
It's the group that develops and advances the social standards that make this network go. It's open to anyone. If you have specific interests, we have task forces that work on specific topics. Or you can start one!
It's easy to join.
w3.org
Anyone who is interested in the #Fediverse and #ActivityPub is welcome to join the Social Web Community Group #SocialCG at the #w3c.
It's the group that develops and advances the social standards that make this network go. It's open to anyone. If you have specific interests, we have task forces that work on specific topics. Or you can start one!
It's easy to join.
w3.org
Right after @berlinfediday is over, I am hosting another edition of
Berlin Federated Network Exploration Circle / #BFNEC
Sept 13th at 7pm at @offline
✸ @pukkamustard will talk about their work on ERIS and give us the backstory on their initiation of the fediverse's FEP process.
⁂ @f will discuss his work on @sutty, a platform aimed at giving organizations and collectives the means to publish and host more secure websites. He will also talk about how they recently added #ActivityPub federation.

Berlin Federated Network Exploration Circle a cosy meeting of humans who want to discuss this strange networked world together September 13th, 2026 (7pm) at Offline ✸ pukkamustard will talk about their work on ERIS and give us the backstory on their initiation of the fediverse's FEP process. ❋ f will discuss his work on Sutty, a platform aimed at giving organizations and collectives the means to publish and host more secure websites. He will also talk about how they recently added ActivityPub federation. ⁂
#FediHood now supports #Lemmy through #ActivityPub. You can subscribe/unsubscribe from groups, read threaded discussions with their upvote scores, and reply or upvote yourself (through the fav button).
This is the first step: FediHood will also automatically split messages into city, region and country groups that everyone on the fediverse using groups will be able to interact with.
Anyone who is interested in the #Fediverse and #ActivityPub is welcome to join the Social Web Community Group #SocialCG at the #w3c.
It's the group that develops and advances the social standards that make this network go. It's open to anyone. If you have specific interests, we have task forces that work on specific topics. Or you can start one!
It's easy to join.
w3.org
I see quite a lot of webfinger requests to my ActivityPub server with the User Agent set to "Identity Collector/3.0.0 (+local defensive OSINT collector)".
Anyone knows what that is about?
I haven’t heard of it. Is it a #fediverse app?
I would love to have a blog that used #activitypub for comments.
So, dann schauen wir mal, ob der Hashtag zieht!
Erinnerung und Einladung zum #fedimainz Kickoff und Brainstorming:
Habt ihr Lust auf Civic Tech? Fedi-apps für Mainz? Zeit am kommenden Samstag Abend? Und könnt da in Mainz sein? Dann kommt vorbei!
Am Samstag, 12.9. um 17 Uhr (mit anschließendem Einkehrschwung ab 18 Uhr, Standort gibt's dann ebenfalls hierunter!) auf dem Mainzer Wissenschaftsmarkt im mainzed-Zelt (beim Landtag!)!
Hier ist ein Anfang einer Ideensammlung, ergänzt gerne: https://pad.gwdg.de/RRq8VIoUTNit8vrD8K1l4Q
Einen Viko-Link versuche ich noch kurzfristig hierunter zu posten.
#Mainz #mainzed #Fediverse #ActivityPub #Wissenschaftsmarkt #fedimainz
pad.gwdg.de
I just added a new watchlist to the fediverse.party wiki, for CMS that have ActivityPub plugins;
https://codeberg.org/fediverse/fediparty/wiki/CMS+with+ActivityPub+plugins+-+watchlist.-
As always, if you see any gaps or errors, sing out!
codeberg.org
:milky_way: A quick look into Fediverse networks -
I see quite a lot of webfinger requests to my ActivityPub server with the User Agent set to "Identity Collector/3.0.0 (+local defensive OSINT collector)".
Anyone knows what that is about?
...
auf dem Fedimainz-Ideenpad (s.o.) habe ich schon ein paar Projekte, Materialien und Ideen von @plinubius , @sl007 , @fabian , @blazr , @pfefferle , @di0v0n , @defnull , @rstockm und anderen hinterlegt.
@lavaeolus , @jonatan , @rupdecat , @fthierygeo , @freifunkmainz , @thk , @Sascha wenn ihr noch weitere Ideen habt, mögt ihr vielleicht auch was eintragen...?
und gerne teilen 😉
#fedimainz #CivicTech #ActivityPub #Fediverse #mastodon #Mainz #Wissenschaftsmarkt
...
auf dem Fedimainz-Ideenpad (s.o.) habe ich schon ein paar Projekte, Materialien und Ideen von @plinubius , @sl007 , @fabian , @blazr , @pfefferle , @di0v0n , @defnull , @rstockm und anderen hinterlegt.
@lavaeolus , @jonatan , @rupdecat , @fthierygeo , @freifunkmainz , @thk , @Sascha wenn ihr noch weitere Ideen habt, mögt ihr vielleicht auch was eintragen...?
und gerne teilen 😉
#fedimainz #CivicTech #ActivityPub #Fediverse #mastodon #Mainz #Wissenschaftsmarkt
I just added a new watchlist to the fediverse.party wiki, for CMS that have ActivityPub plugins;
https://codeberg.org/fediverse/fediparty/wiki/CMS+with+ActivityPub+plugins+-+watchlist.-
As always, if you see any gaps or errors, sing out!
codeberg.org
:milky_way: A quick look into Fediverse networks -
The EFF is promoting the Fediverse in an online event next week!
We've put it on our community calendar: https://fediforum.org/events
fediforum.org
Events anywhere that focus on the Open Social Web, including but not limited to FediForum.
I haven’t heard of it. Is it a #fediverse app?
I would love to have a blog that used #activitypub for comments.
So, dann schauen wir mal, ob der Hashtag zieht!
Erinnerung und Einladung zum #fedimainz Kickoff und Brainstorming:
Habt ihr Lust auf Civic Tech? Fedi-apps für Mainz? Zeit am kommenden Samstag Abend? Und könnt da in Mainz sein? Dann kommt vorbei!
Am Samstag, 12.9. um 17 Uhr (mit anschließendem Einkehrschwung ab 18 Uhr, Standort gibt's dann ebenfalls hierunter!) auf dem Mainzer Wissenschaftsmarkt im mainzed-Zelt (beim Landtag!)!
Hier ist ein Anfang einer Ideensammlung, ergänzt gerne: https://pad.gwdg.de/RRq8VIoUTNit8vrD8K1l4Q
Einen Viko-Link versuche ich noch kurzfristig hierunter zu posten.
#Mainz #mainzed #Fediverse #ActivityPub #Wissenschaftsmarkt #fedimainz
pad.gwdg.de
Kind of a weird #Fediverse question: do any platforms in this space offer an API for user account registration?
One hypothetical idea I’ve been kicking around is the notion of “pre-registration” for the Fediverse. Basically, a person fills out their profile, imports data archives from other networks, and maybe even finds their mutuals somehow. When they’re done, their account gets created on a random vetted instance, data gets backfilled, and follow requests get sent out to the mutual contacts they’ve found.
I think it’s a good idea, and possibly a good showcase for both the #ActivityPub C2S API and possibly LOLA. However, I don’t think any particular platform supports all of the required pieces yet.

A mockup of an app, showing my name and face, as well as different social networks to connect to for data import.

Next step in the mockup for a Facebook import, allowing me to pick and choose what data is most important to me.

A final stage mockup showing a recommended server with its name, domain, and rules. Obviously needs a touch up, but the idea is that everything is spelled out to the person about where they're going.
Kind of a weird #Fediverse question: do any platforms in this space offer an API for user account registration?
One hypothetical idea I’ve been kicking around is the notion of “pre-registration” for the Fediverse. Basically, a person fills out their profile, imports data archives from other networks, and maybe even finds their mutuals somehow. When they’re done, their account gets created on a random vetted instance, data gets backfilled, and follow requests get sent out to the mutual contacts they’ve found.
I think it’s a good idea, and possibly a good showcase for both the #ActivityPub C2S API and possibly LOLA. However, I don’t think any particular platform supports all of the required pieces yet.

A mockup of an app, showing my name and face, as well as different social networks to connect to for data import.

Next step in the mockup for a Facebook import, allowing me to pick and choose what data is most important to me.

A final stage mockup showing a recommended server with its name, domain, and rules. Obviously needs a touch up, but the idea is that everything is spelled out to the person about where they're going.
Kind of a weird #Fediverse question: do any platforms in this space offer an API for user account registration?
One hypothetical idea I’ve been kicking around is the notion of “pre-registration” for the Fediverse. Basically, a person fills out their profile, imports data archives from other networks, and maybe even finds their mutuals somehow. When they’re done, their account gets created on a random vetted instance, data gets backfilled, and follow requests get sent out to the mutual contacts they’ve found.
I think it’s a good idea, and possibly a good showcase for both the #ActivityPub C2S API and possibly LOLA. However, I don’t think any particular platform supports all of the required pieces yet.

A mockup of an app, showing my name and face, as well as different social networks to connect to for data import.

Next step in the mockup for a Facebook import, allowing me to pick and choose what data is most important to me.

A final stage mockup showing a recommended server with its name, domain, and rules. Obviously needs a touch up, but the idea is that everything is spelled out to the person about where they're going.
The EFF is promoting the Fediverse in an online event next week!
We've put it on our community calendar: https://fediforum.org/events
fediforum.org
Events anywhere that focus on the Open Social Web, including but not limited to FediForum.
Indeed. Over the years I've seen so many developers become discouraged to continue their #ActivityPub projects after becoming initially highly excited upon first reading the #W3C specs. They bump into a world of undocumented complexity, harsh wire realities, inconsistencies and tech debt. Only the true "stayers" grit their teeth and keep at it. Developers love to be able to focus on a solution, and not get mired into all kinds of low-level plumbing that is not related to that.
This makes tech that's social-culturally worse (ATProto w/ centralization, VC capital) still have better uptake potential.
I gave special mention to both @fedify and @Bonfire in my article. For #Fedify for how they focus on providing a framework and SDK that hands people software so that they can focus on "solution development" immediately. And #Bonfire because they constitute a framework, but also engage directly with clients and partners in solution development, closest to making the paradigm shift.
Are there any bridges for EhNow! or NorthSocial? Or better yet, might these be federated?
I can’t seem to find information pointing to either of these networks supporting activity-pub, but maybe I’m just doing it wrong.
#Canada #DigitalSovereignty #ActivityPub #Bridge #EhNow #NorthSocial
Are there any bridges for EhNow! or NorthSocial? Or better yet, might these be federated?
I can’t seem to find information pointing to either of these networks supporting activity-pub, but maybe I’m just doing it wrong.
#Canada #DigitalSovereignty #ActivityPub #Bridge #EhNow #NorthSocial
The EFF is promoting the Fediverse in an online event next week!
We've put it on our community calendar: https://fediforum.org/events
fediforum.org
Events anywhere that focus on the Open Social Web, including but not limited to FediForum.
The EFF is promoting the Fediverse in an online event next week!
We've put it on our community calendar: https://fediforum.org/events
fediforum.org
Events anywhere that focus on the Open Social Web, including but not limited to FediForum.
The EFF is promoting the Fediverse in an online event next week!
We've put it on our community calendar: https://fediforum.org/events
fediforum.org
Events anywhere that focus on the Open Social Web, including but not limited to FediForum.
Right after @berlinfediday is over, I am hosting another edition of
Berlin Federated Network Exploration Circle / #BFNEC
Sept 13th at 7pm at @offline
✸ @pukkamustard will talk about their work on ERIS and give us the backstory on their initiation of the fediverse's FEP process.
⁂ @f will discuss his work on @sutty, a platform aimed at giving organizations and collectives the means to publish and host more secure websites. He will also talk about how they recently added #ActivityPub federation.

Berlin Federated Network Exploration Circle a cosy meeting of humans who want to discuss this strange networked world together September 13th, 2026 (7pm) at Offline ✸ pukkamustard will talk about their work on ERIS and give us the backstory on their initiation of the fediverse's FEP process. ❋ f will discuss his work on Sutty, a platform aimed at giving organizations and collectives the means to publish and host more secure websites. He will also talk about how they recently added ActivityPub federation. ⁂
FediForum Fall 2026 starts in four weeks, exactly.
Registrations and proposed topics are coming. Check some of what has been proposed so far on https://fediforum.org/2026-10/
fediforum.org
Registration is open. Propose sessions. Suggest cool demos.
I've now found two different #ActivityPub implementations which *don't* send a message digest in their headers.
That's much more of a risk than a long clock skew, I think.
I built an open-source analytics terminal tracking daily Mastodon user growth, monthly active users (MAU), and velocity from 2022 to present:
https://obsesivegamer.github.io/Mastodon-User-Trends/
Features:
- Dual-mode daily net velocity (MAU health vs signups)
- Historical surge windows (Nov '22 Twitter wave, '23 Threads surge)
- Trailing 7-day moving averages
- Free CSV data exports
#Mastodon #Fediverse #MastodonStats #DataViz #ActivityPub #OpenSource
obsesivegamer.github.io
Interactive quantitative analytics terminal tracking Mastodon user growth, active user retention, and daily network velocity.
Technology wars are generally unproductive and unconstructive. At the same time there's undeniably competition - #ActivityPub vs #ATProto - or coopetition - ActivityPub vs #XMPP - between different technologies. Because success and popular uptake of one technology can lead to the demise of another.
Instead of diminishing the other #technology and engage in endless heated fights and conflicts, it is better to consider the technology one is most passionate about, and look for all those aspects and factors that are important for its uptake and widespread #adoption. Stand strong in your own shoes. Other #technologies then become input to learn from. What are weaknesses, what can be copied, and what can be done better? And also, where can there be unison, interoperability, collaboration, cross-pollination with other technology ecosystems?
Technology doesn't "win". It's people who stand to win or loose, depending on how well the tech supports their needs.
Technology wars are generally unproductive and unconstructive. At the same time there's undeniably competition - #ActivityPub vs #ATProto - or coopetition - ActivityPub vs #XMPP - between different technologies. Because success and popular uptake of one technology can lead to the demise of another.
Instead of diminishing the other #technology and engage in endless heated fights and conflicts, it is better to consider the technology one is most passionate about, and look for all those aspects and factors that are important for its uptake and widespread #adoption. Stand strong in your own shoes. Other #technologies then become input to learn from. What are weaknesses, what can be copied, and what can be done better? And also, where can there be unison, interoperability, collaboration, cross-pollination with other technology ecosystems?
Technology doesn't "win". It's people who stand to win or loose, depending on how well the tech supports their needs.
Indeed. Over the years I've seen so many developers become discouraged to continue their #ActivityPub projects after becoming initially highly excited upon first reading the #W3C specs. They bump into a world of undocumented complexity, harsh wire realities, inconsistencies and tech debt. Only the true "stayers" grit their teeth and keep at it. Developers love to be able to focus on a solution, and not get mired into all kinds of low-level plumbing that is not related to that.
This makes tech that's social-culturally worse (ATProto w/ centralization, VC capital) still have better uptake potential.
I gave special mention to both @fedify and @Bonfire in my article. For #Fedify for how they focus on providing a framework and SDK that hands people software so that they can focus on "solution development" immediately. And #Bonfire because they constitute a framework, but also engage directly with clients and partners in solution development, closest to making the paradigm shift.
🆕 blog! “ActivityPub - Is it worth defending against replay attacks and message/signature time skew?”
Here's a problem that I've found with ActivityBot - my little ActivityPub server. Sometimes it receives messages which were originally sent months ago. Why does that happen and is it risky to accept and…
👀 Read more: https://shkspr.mobi/blog/2026/09/activitypub-is-it-worth-defending-against-replay-attacks-and-message-signature-time-skew/
⸻
#ActivityBot #ActivityPub #http #security
shkspr.mobi
Here's a problem that I've found with ActivityBot - my little ActivityPub server. Sometimes it receives messages which were originally sent months ago. Why does that happen and is it risky to accept and process them? My tl;dr is that it probably isn't worth worrying about. But I'd love someone to tell me why I'm wrong. Here's my thinking: Table of ContentsCausesIs that a problem?What are we…
Right after @berlinfediday is over, I am hosting another edition of
Berlin Federated Network Exploration Circle / #BFNEC
Sept 13th at 7pm at @offline
✸ @pukkamustard will talk about their work on ERIS and give us the backstory on their initiation of the fediverse's FEP process.
⁂ @f will discuss his work on @sutty, a platform aimed at giving organizations and collectives the means to publish and host more secure websites. He will also talk about how they recently added #ActivityPub federation.

Berlin Federated Network Exploration Circle a cosy meeting of humans who want to discuss this strange networked world together September 13th, 2026 (7pm) at Offline ✸ pukkamustard will talk about their work on ERIS and give us the backstory on their initiation of the fediverse's FEP process. ❋ f will discuss his work on Sutty, a platform aimed at giving organizations and collectives the means to publish and host more secure websites. He will also talk about how they recently added ActivityPub federation. ⁂
I built an open-source analytics terminal tracking daily Mastodon user growth, monthly active users (MAU), and velocity from 2022 to present:
https://obsesivegamer.github.io/Mastodon-User-Trends/
Features:
- Dual-mode daily net velocity (MAU health vs signups)
- Historical surge windows (Nov '22 Twitter wave, '23 Threads surge)
- Trailing 7-day moving averages
- Free CSV data exports
#Mastodon #Fediverse #MastodonStats #DataViz #ActivityPub #OpenSource
obsesivegamer.github.io
Interactive quantitative analytics terminal tracking Mastodon user growth, active user retention, and daily network velocity.
I've now found two different #ActivityPub implementations which *don't* send a message digest in their headers.
That's much more of a risk than a long clock skew, I think.
🆕 blog! “ActivityPub - Is it worth defending against replay attacks and message/signature time skew?”
Here's a problem that I've found with ActivityBot - my little ActivityPub server. Sometimes it receives messages which were originally sent months ago. Why does that happen and is it risky to accept and…
👀 Read more: https://shkspr.mobi/blog/2026/09/activitypub-is-it-worth-defending-against-replay-attacks-and-message-signature-time-skew/
⸻
#ActivityBot #ActivityPub #http #security
shkspr.mobi
Here's a problem that I've found with ActivityBot - my little ActivityPub server. Sometimes it receives messages which were originally sent months ago. Why does that happen and is it risky to accept and process them? My tl;dr is that it probably isn't worth worrying about. But I'd love someone to tell me why I'm wrong. Here's my thinking: Table of ContentsCausesIs that a problem?What are we…
FediForum Fall 2026 starts in four weeks, exactly.
Registrations and proposed topics are coming. Check some of what has been proposed so far on https://fediforum.org/2026-10/
fediforum.org
Registration is open. Propose sessions. Suggest cool demos.
Right after @berlinfediday is over, I am hosting another edition of
Berlin Federated Network Exploration Circle / #BFNEC
Sept 13th at 7pm at @offline
✸ @pukkamustard will talk about their work on ERIS and give us the backstory on their initiation of the fediverse's FEP process.
⁂ @f will discuss his work on @sutty, a platform aimed at giving organizations and collectives the means to publish and host more secure websites. He will also talk about how they recently added #ActivityPub federation.

Berlin Federated Network Exploration Circle a cosy meeting of humans who want to discuss this strange networked world together September 13th, 2026 (7pm) at Offline ✸ pukkamustard will talk about their work on ERIS and give us the backstory on their initiation of the fediverse's FEP process. ❋ f will discuss his work on Sutty, a platform aimed at giving organizations and collectives the means to publish and host more secure websites. He will also talk about how they recently added ActivityPub federation. ⁂
Right after @berlinfediday is over, I am hosting another edition of
Berlin Federated Network Exploration Circle / #BFNEC
Sept 13th at 7pm at @offline
✸ @pukkamustard will talk about their work on ERIS and give us the backstory on their initiation of the fediverse's FEP process.
⁂ @f will discuss his work on @sutty, a platform aimed at giving organizations and collectives the means to publish and host more secure websites. He will also talk about how they recently added #ActivityPub federation.

Berlin Federated Network Exploration Circle a cosy meeting of humans who want to discuss this strange networked world together September 13th, 2026 (7pm) at Offline ✸ pukkamustard will talk about their work on ERIS and give us the backstory on their initiation of the fediverse's FEP process. ❋ f will discuss his work on Sutty, a platform aimed at giving organizations and collectives the means to publish and host more secure websites. He will also talk about how they recently added ActivityPub federation. ⁂
🆕 blog! “ActivityPub - Is it worth defending against replay attacks and message/signature time skew?”
Here's a problem that I've found with ActivityBot - my little ActivityPub server. Sometimes it receives messages which were originally sent months ago. Why does that happen and is it risky to accept and…
👀 Read more: https://shkspr.mobi/blog/2026/09/activitypub-is-it-worth-defending-against-replay-attacks-and-message-signature-time-skew/
⸻
#ActivityBot #ActivityPub #http #security
shkspr.mobi
Here's a problem that I've found with ActivityBot - my little ActivityPub server. Sometimes it receives messages which were originally sent months ago. Why does that happen and is it risky to accept and process them? My tl;dr is that it probably isn't worth worrying about. But I'd love someone to tell me why I'm wrong. Here's my thinking: Table of ContentsCausesIs that a problem?What are we…
How this is implemented is crucial for the future of the #fediverse. An app having app-specific integrations to some - indeed ethical - payment systems, is one thing. But for instance offering native #monetization features on #ActivityPub protocol level, and not thinking *very hard* about the implementation and impact of that, can totally enshittify the #fedi in no time in how it attracts the wrong type of people with dollar signs in their eyes.
The current app-centric fediverse is the playground of devs and "users" that have to swallow what they create and drop onto the wire. #FOSS culture is a poor model for the evolution of the fediverse, where the latter is an existing social space where people, the online residents, have their home. It is much more than a pure technosphere and playground for app devs.
Fedi isn't ready to be growth-hacked. If successful it'll undoubtedly see corporate capture with Corporate Fedi the outcome, same as the web.

coding.social
Only humans make the social web social.
do australian fedi conferences exist? I've seen a few but they're always at like, 1am aest
do australian fedi conferences exist? I've seen a few but they're always at like, 1am aest
My most recent blog post deals with this subject matter as well, and how we might make a paradigm shift to move away from this app-centric #fediverse where "users" (i.e. people) hardly have a say in its evolution. The efforts around the #ActivityPub API may be an enabler for this shift, but there are major social challenges to be dealt with.
https://coding.social/blog/prosocial-web/
Personally I don't think the app-centric fediverse has a healthy future or outcome.

coding.social
Only humans make the social web social.
How this is implemented is crucial for the future of the #fediverse. An app having app-specific integrations to some - indeed ethical - payment systems, is one thing. But for instance offering native #monetization features on #ActivityPub protocol level, and not thinking *very hard* about the implementation and impact of that, can totally enshittify the #fedi in no time in how it attracts the wrong type of people with dollar signs in their eyes.
The current app-centric fediverse is the playground of devs and "users" that have to swallow what they create and drop onto the wire. #FOSS culture is a poor model for the evolution of the fediverse, where the latter is an existing social space where people, the online residents, have their home. It is much more than a pure technosphere and playground for app devs.
Fedi isn't ready to be growth-hacked. If successful it'll undoubtedly see corporate capture with Corporate Fedi the outcome, same as the web.

coding.social
Only humans make the social web social.
How is this possible? I hope the plan is not to rely on US banks in light of the A/I takedown. #Fediverse #Ethics #Mastodon #Peertube #Loops @dansup #pixelfed #LoopsIsWorkingOnMonetization to get paid. #ActivityPub #AutisticiInventati
mastodon.social
Attached: 1 image Loops is working on bringing monetization to the fediverse to get creators paid! Loops Studio will become creators best friend, more details soon. #Loops #EthicalMonetization
How is this possible? I hope the plan is not to rely on US banks in light of the A/I takedown. #Fediverse #Ethics #Mastodon #Peertube #Loops @dansup #pixelfed #LoopsIsWorkingOnMonetization to get paid. #ActivityPub #AutisticiInventati
mastodon.social
Attached: 1 image Loops is working on bringing monetization to the fediverse to get creators paid! Loops Studio will become creators best friend, more details soon. #Loops #EthicalMonetization
Hello #activitypub people - question: Why does Mastodon selectively show only some tags under image while there are many tags in the post itself?
Post to investigate:
https://bots.ambience.sk/@drinksnomore/d62733b3-4fe4-43d5-8973-a119fe0ac8e5
#mastodon #mastoadmin #tech #php #webdev #ap #tags #question
I'll never get over how cool it is that I can comment on other's websites with my Mastodon server. ActivityPub is awesome! 💪

Screenshot of Open Mentions' website where Matt asks for a Question of the Week: "Wherever you are reading this question, what is your story of how you came to interact there? For example, if we are talking on Mastodon, why did you join? If on a blog, why did you start it (or join it)?" I reply from my Mastodon server: "I found openmentions.com recently on fediverse.observer looking for blogs to follow from my Mastodon account. I think it's so cool how with ActivityPub, I can interact with this post and have my comment appear on your website! As for my server, I've been running it for almost four years now. I was in the November 2022 Twitter exodus and began my Mastodon journey on mas.to. I loved it so much that I wanted to make my own server to continue sharing my music and art with others." My username and my profile picture of an anthropomorphic turquoise cartoon badger with a blue hoodie appears as well in the comments, likes, and reposts sections.
Ha, @loremipsum I love your anti-bot tag cloud. 😆
Do you mind, if I make a reference to it in an issue on #Botiquette I created?
https://codeberg.org/fediverse/fediverse-ideas/issues/33#issuecomment-13221306
The Bio field shouldn't be for arbitrary app-specific #hashtag control words, that devs invent on-the-fly to drive their custom opt-out or opt-in mechanisms.
social.coop
Attached: 1 image #AntiPatterned™ Do NOT create out-of-bound custom and app-centric mechanisms that define new and expected behavior on protocol level. https://codeberg.org/fediverse/fediverse-ideas/issues/33 #SX #SocialCoding #SocialWeb #ActivityPub #ProtocolDecay #Botiquette
Do NOT create out-of-bound custom and app-centric mechanisms that define new and unexpected behavior on protocol level.
https://codeberg.org/fediverse/fediverse-ideas/issues/33
#SX #SocialCoding #SocialWeb #ActivityPub #ProtocolDecay #Botiquette
I'll never get over how cool it is that I can comment on other's websites with my Mastodon server. ActivityPub is awesome! 💪

Screenshot of Open Mentions' website where Matt asks for a Question of the Week: "Wherever you are reading this question, what is your story of how you came to interact there? For example, if we are talking on Mastodon, why did you join? If on a blog, why did you start it (or join it)?" I reply from my Mastodon server: "I found openmentions.com recently on fediverse.observer looking for blogs to follow from my Mastodon account. I think it's so cool how with ActivityPub, I can interact with this post and have my comment appear on your website! As for my server, I've been running it for almost four years now. I was in the November 2022 Twitter exodus and began my Mastodon journey on mas.to. I loved it so much that I wanted to make my own server to continue sharing my music and art with others." My username and my profile picture of an anthropomorphic turquoise cartoon badger with a blue hoodie appears as well in the comments, likes, and reposts sections.
Well, after the issues with the given ActivityPub "OAuth example", I looked up
@fedify /vocab …
In all the actor types like https://github.com/fedify-dev/fedify/blob/main/packages/vocab/src/person.yaml
the following properties would be missing [if the official OAuth demo should work] :
grant_types_supported: ['authorization_code'],
response_types_supported:
['code', 'code token'],
code_challenge_methods_supported: ['S256'],
token_endpoint_auth_methods_supported: [
'client_secret_basic'
// optional
/*, 'private_key_jwt'*/
],
token_endpoint_auth_signing_alg_values_supported: ['RS256', 'ES256'],
client_id_metadata_document_supported: true,
optional
scopes_supported and ui_locales_supported:
---
anyway:
I believe it is just misleading because of the order of querying the server and the actor description.
I would expect it to meet the spec and pull above properties from the server wide .well-known oauth and just the endpoints from the Actor …
github.com
ActivityPub server framework in TypeScript. Contribute to fedify-dev/fedify development by creating an account on GitHub.
Ha, @loremipsum I love your anti-bot tag cloud. 😆
Do you mind, if I make a reference to it in an issue on #Botiquette I created?
https://codeberg.org/fediverse/fediverse-ideas/issues/33#issuecomment-13221306
The Bio field shouldn't be for arbitrary app-specific #hashtag control words, that devs invent on-the-fly to drive their custom opt-out or opt-in mechanisms.
social.coop
Attached: 1 image #AntiPatterned™ Do NOT create out-of-bound custom and app-centric mechanisms that define new and expected behavior on protocol level. https://codeberg.org/fediverse/fediverse-ideas/issues/33 #SX #SocialCoding #SocialWeb #ActivityPub #ProtocolDecay #Botiquette
Do NOT create out-of-bound custom and app-centric mechanisms that define new and unexpected behavior on protocol level.
https://codeberg.org/fediverse/fediverse-ideas/issues/33
#SX #SocialCoding #SocialWeb #ActivityPub #ProtocolDecay #Botiquette
is there a FLOSS app for Android that support both activitypub and ATproto accounts in one interface?
is there a FLOSS app for Android that support both activitypub and ATproto accounts in one interface?
Woot, a rave review by @thefulcrum 💞 on my blog post "Paradigm shift to the Prosocial web".
https://www.thefulcrum.dev/the-programmers-fulcrum-4-september-2026/
Thank you very much!
I have some resources queued up for addition to the delightful #fediverse development curated at:
https://delightful.coding.social/delightful-fediverse-development
@Steve_Bate created an #ActivityPub vocabulary using #LinkedData and wrote an article about it:
https://www.stevebate.net/activitypub-ontology/
And also he created a #JSONSchema project for the #fediverse:
https://github.com/steve-bate/fediverse-jsonschema
Then there are 2 vocabulary projects in code that I am aware of, one in #Rust and one in #TypeScript:
@maddyunderstars forked and extends activitypub-types:
https://github.com/MaddyUnderStars/activitypub-types
And @weathered_steel@weathered_steel.social wrote:
https://codeberg.org/activitypub-rs/activitystreams-vocabulary
codeberg.org
ActivityStreams 2.0 Vocabulary data structures
It's a real bummer that #Mastodon won't allow me to share an #ActivityPub -enabled #WordPress post that has images. At this stage I'd be better able to share my posts if I removed the ActivityPub integration!
I have some resources queued up for addition to the delightful #fediverse development curated at:
https://delightful.coding.social/delightful-fediverse-development
@Steve_Bate created an #ActivityPub vocabulary using #LinkedData and wrote an article about it:
https://www.stevebate.net/activitypub-ontology/
And also he created a #JSONSchema project for the #fediverse:
https://github.com/steve-bate/fediverse-jsonschema
Then there are 2 vocabulary projects in code that I am aware of, one in #Rust and one in #TypeScript:
@maddyunderstars forked and extends activitypub-types:
https://github.com/MaddyUnderStars/activitypub-types
And @weathered_steel@weathered_steel.social wrote:
https://codeberg.org/activitypub-rs/activitystreams-vocabulary
codeberg.org
ActivityStreams 2.0 Vocabulary data structures
Today I am mostly learning PHPUnit so I can add some tests to #ActivityBot.
Interesting to see what assumptions I've made along the way and what the consequences are.
I also wish there were a schema for #ActivityPub JSON which I could use to test my outputs.
Today I am mostly learning PHPUnit so I can add some tests to #ActivityBot.
Interesting to see what assumptions I've made along the way and what the consequences are.
I also wish there were a schema for #ActivityPub JSON which I could use to test my outputs.
"…the #ActivityPub API is one of the most promising areas of #innovation for #thefediverse. This #standardization effort around the client-to-server specification profile of the #W3C ActivityPub recommendation allows a further decoupling of client and server software.
…
Unfortunately there is no real consensus on what the #ActivityPubAPI should offer, and there is little coordination between stakeholders, in true grassroots fashion."
https://coding.social/blog/prosocial-web/

coding.social
Only humans make the social web social.
"…the #ActivityPub API is one of the most promising areas of #innovation for #thefediverse. This #standardization effort around the client-to-server specification profile of the #W3C ActivityPub recommendation allows a further decoupling of client and server software.
…
Unfortunately there is no real consensus on what the #ActivityPubAPI should offer, and there is little coordination between stakeholders, in true grassroots fashion."
https://coding.social/blog/prosocial-web/

coding.social
Only humans make the social web social.
Public Messaging Platforms
‣ Simple: Plain old website with #RSS feeds
‣ Emergency Alerts: #CAP (Common Alerting Protocol) + delivery backends like #CellBroadcast
‣ Microblogging: #Mastodon (AP)
‣ Long form content: #WordPress + AP plugin
‣ Video hosting: #Peertube (AP)
Notes:
• AP: #ActivityPub
• This is only a small sample of the options available.
• These platforms are in use globally for this purpose.
For #ActivityPub applications people may refer to the three deligthful #fediverse curated lists I maintain. See the ToC in the top-level list at:
delightful.coding.social
Public Messaging Platforms
‣ Simple: Plain old website with #RSS feeds
‣ Emergency Alerts: #CAP (Common Alerting Protocol) + delivery backends like #CellBroadcast
‣ Microblogging: #Mastodon (AP)
‣ Long form content: #WordPress + AP plugin
‣ Video hosting: #Peertube (AP)
Notes:
• AP: #ActivityPub
• This is only a small sample of the options available.
• These platforms are in use globally for this purpose.
Sometimes there seems to be a belief in some places that #ActivityPub standardization stopped years ago. True, it was in a lull for some time.
But in recent years, it has become quite active again.
Check out this table.
w3c.social
What might the Social Web Community Group be up to these days? There are about a dozen (!!) active Task Forces, working on a broad spectrum of protocols, vocabularies, conventions, best practices and the like all centered around the core ActivityPub standard. To make it easier to find out what those Task Forces are and what they do, we now have a summary table: https://github.com/swicg/meetings/blob/main/DELIVERABLES-TASKFORCES.md Sounds interesting? Maybe you'll join us on the mailing list or in a meeting soon. https://www.w3.org/community/socialcg/
What might the Social Web Community Group be up to these days?
There are about a dozen (!!) active Task Forces, working on a broad spectrum of protocols, vocabularies, conventions, best practices and the like all centered around the core ActivityPub standard.
To make it easier to find out what those Task Forces are and what they do, we now have a summary table:
https://github.com/swicg/meetings/blob/main/DELIVERABLES-TASKFORCES.md
Sounds interesting? Maybe you'll join us on the mailing list or in a meeting soon.
w3.org
Sometimes there seems to be a belief in some places that #ActivityPub standardization stopped years ago. True, it was in a lull for some time.
But in recent years, it has become quite active again.
Check out this table.
w3c.social
What might the Social Web Community Group be up to these days? There are about a dozen (!!) active Task Forces, working on a broad spectrum of protocols, vocabularies, conventions, best practices and the like all centered around the core ActivityPub standard. To make it easier to find out what those Task Forces are and what they do, we now have a summary table: https://github.com/swicg/meetings/blob/main/DELIVERABLES-TASKFORCES.md Sounds interesting? Maybe you'll join us on the mailing list or in a meeting soon. https://www.w3.org/community/socialcg/
What might the Social Web Community Group be up to these days?
There are about a dozen (!!) active Task Forces, working on a broad spectrum of protocols, vocabularies, conventions, best practices and the like all centered around the core ActivityPub standard.
To make it easier to find out what those Task Forces are and what they do, we now have a summary table:
https://github.com/swicg/meetings/blob/main/DELIVERABLES-TASKFORCES.md
Sounds interesting? Maybe you'll join us on the mailing list or in a meeting soon.
w3.org
Should publishing with ActivityPub be about as easy as publishing with RSS?
An article was submitted to HN how #Authorization terminology is a mess and proposed a way to fix it.
Top-level comment started a discussion on the difference between SHOULD and OUGHT TO. The latter expresses a moral imperative.
SHOULD is defined in #RFC2119 which is referenced in all #ActivityPub specs and #FEPs.
Now, there is an April Fool's RFC6919 which defines OUGHT TO. While this RFC is created just for fun, to express all kinds of wriggle room to a spec, which is often the reality in a codebase, the OUGHT TO indicator is not all that bad for actual use in #fediverse specs.
The fediverse is more than cold, hard technology space. It is a place where particular values and principles are held dear, moral imperatives to build better alternatives to social media.
An ought combines with a should like this:
"Privacy OUGHT TO be protected first and foremost, therefore an opt-in mechanism SHOULD be provided"
https://idpro.org/authorization-terminology-is-a-mess-lets-fix-it/
The ActivityPoll #FEP has a very unfortunate name, clashing and overloading with the existing concept of a #Poll on the fediverse. The mechanism it documents is generally called #Polling.
helge.codeberg.page
ActivityPoll is a proper subset of ActivityPub that excludes activity delivery, making it easier to implement for static Web sites or content management systems. It meets an equivalent need to RSS or Atom feeds.
Yes, but...
I understand the RSS and Atom specs well enough that I've created programs with RSS/Atom feeds. But the ActivityPub spec scares the willies out of me. Too many moving parts.
Yes, but...
I understand the RSS and Atom specs well enough that I've created programs with RSS/Atom feeds. But the ActivityPub spec scares the willies out of me. Too many moving parts.
ActivityPub, the standard that powers the fediverse, continues to evolve and we all can contribute!
github.com
Contribute to w3c/activitypub development by creating an account on GitHub.
ActivityPub, the standard that powers the fediverse, continues to evolve and we all can contribute!
github.com
Contribute to w3c/activitypub development by creating an account on GitHub.
"There are several possible dates you might pick, but for me it will always be September 5th 2014 - when I committed the first sketch of a specification I called ActivityPump and pushed it to Github."
Happy 12th birthday, ActivityPub!
https://akko.erincandescent.net/objects/fbdd0aca-87ae-4d58-83b2-ee95dde2e229
#ActivityPub #fediverse #anniversary #SocialMedia #standards
akko.erincandescent.net
The obvious choice for ActivityPub’s birthday would be the 23rd of January 2018 - the day it was annointed as a W3C recommendation. That doesn’t seem quite right though - its not as if the spec cam...
ActivityPub, the standard that powers the fediverse, continues to evolve and we all can contribute!
github.com
Contribute to w3c/activitypub development by creating an account on GitHub.
"There are several possible dates you might pick, but for me it will always be September 5th 2014 - when I committed the first sketch of a specification I called ActivityPump and pushed it to Github."
Happy 12th birthday, ActivityPub!
https://akko.erincandescent.net/objects/fbdd0aca-87ae-4d58-83b2-ee95dde2e229
#ActivityPub #fediverse #anniversary #SocialMedia #standards
akko.erincandescent.net
The obvious choice for ActivityPub’s birthday would be the 23rd of January 2018 - the day it was annointed as a W3C recommendation. That doesn’t seem quite right though - its not as if the spec cam...
Arguably #fediverse is NOT based on an open standard.
It's based on 'guidance towards an open standard' via the incomplete #ActivityPub spec, sprinkled with what fediverse devs have added over time via post-facto interoperability in what's effectively a Big ball of mud anti-pattern. These extra's are to be discovered on-the-fly, and being app-specific, need to be maintained constantly in another anti-pattern I dubbed Whack-a-mole programming.
We can do better. We can rise above the app-centricness of the fedi, make a paradigm shift that helps increase evolution speed and rate of innovation and sees people's needs be better served..
https://coding.social/blog/prosocial-web
For that to happen major social challenges need to be tackled, like coordinating and collaborating well in a good grassroots standardization process.
I posed the concept of a Grassroots open standard, where services encapsulate their design, and the standardization process is a fedi-native service itself.

coding.social
Social dynamics in the grassroots fediverse ecosystem and laisséz-faire practices led to divergence from power and promise of the ActivityPub protocol. Grassroots standards and the ActivityPub API initiative can get us back on track.
I have a buncha questions! I will intersperse where relevant.
I am looking at some #indieweb integrations which mix #blog and #ActivityPub - a sort of 1-person masto instance, where responses become comments. Have you looked at any of these?
For me, most of my entries are not public but more journalling, so I am looking for something more complex and secure, yet integrating federated content sharing.
An article was submitted to HN how #Authorization terminology is a mess and proposed a way to fix it.
Top-level comment started a discussion on the difference between SHOULD and OUGHT TO. The latter expresses a moral imperative.
SHOULD is defined in #RFC2119 which is referenced in all #ActivityPub specs and #FEPs.
Now, there is an April Fool's RFC6919 which defines OUGHT TO. While this RFC is created just for fun, to express all kinds of wriggle room to a spec, which is often the reality in a codebase, the OUGHT TO indicator is not all that bad for actual use in #fediverse specs.
The fediverse is more than cold, hard technology space. It is a place where particular values and principles are held dear, moral imperatives to build better alternatives to social media.
An ought combines with a should like this:
"Privacy OUGHT TO be protected first and foremost, therefore an opt-in mechanism SHOULD be provided"
https://idpro.org/authorization-terminology-is-a-mess-lets-fix-it/
Servers
- Ktistec v3.12.0
- Mastodon v4.7.1
- Vernissage v1.43.0
- snac v2.95
- ActivityPub for WordPress v9.3.0
- Owncast v0.3.0
- gathio v1.6.6
- NodeBB v4.15.2
- tootik v0.25.2
- NeoDB v0.18.0
- Bonfire v1.0.7
Clients
- PleromaFE v2.11.4
- Fedilab v3.43.3
- Tuba v0.11.1
- Pixelix v5.2.0
- Summit v1.85.0
- Shoot Web App v1.3.0
Tools and Plugins
- FediFetcher v8.2.0
- Poduptime v6.3.2
- Enable Mastodon Apps v1.6.3 (WordPress plugin)
For developers
- APx v0.27.0
- Library progress report - September 2026 (GoActivityPub)
Articles
- A reasonably practical guide to validating RFC 9421 HTTP Signatures for ActivityPub in PHP
-----
#WeekInFediverse #Fediverse #ActivityPub
Previous edition: https://mitra.social/objects/01a04a26-eede-72b0-a5e9-f244c3ff2142
Servers
- Ktistec v3.12.0
- Mastodon v4.7.1
- Vernissage v1.43.0
- snac v2.95
- ActivityPub for WordPress v9.3.0
- Owncast v0.3.0
- gathio v1.6.6
- NodeBB v4.15.2
- tootik v0.25.2
- NeoDB v0.18.0
- Bonfire v1.0.7
Clients
- PleromaFE v2.11.4
- Fedilab v3.43.3
- Tuba v0.11.1
- Pixelix v5.2.0
- Summit v1.85.0
- Shoot Web App v1.3.0
Tools and Plugins
- FediFetcher v8.2.0
- Poduptime v6.3.2
- Enable Mastodon Apps v1.6.3 (WordPress plugin)
For developers
- APx v0.27.0
- Library progress report - September 2026 (GoActivityPub)
Articles
- A reasonably practical guide to validating RFC 9421 HTTP Signatures for ActivityPub in PHP
-----
#WeekInFediverse #Fediverse #ActivityPub
Previous edition: https://mitra.social/objects/01a04a26-eede-72b0-a5e9-f244c3ff2142
Servers
- Ktistec v3.12.0
- Mastodon v4.7.1
- Vernissage v1.43.0
- snac v2.95
- ActivityPub for WordPress v9.3.0
- Owncast v0.3.0
- gathio v1.6.6
- NodeBB v4.15.2
- tootik v0.25.2
- NeoDB v0.18.0
- Bonfire v1.0.7
Clients
- PleromaFE v2.11.4
- Fedilab v3.43.3
- Tuba v0.11.1
- Pixelix v5.2.0
- Summit v1.85.0
- Shoot Web App v1.3.0
Tools and Plugins
- FediFetcher v8.2.0
- Poduptime v6.3.2
- Enable Mastodon Apps v1.6.3 (WordPress plugin)
For developers
- APx v0.27.0
- Library progress report - September 2026 (GoActivityPub)
Articles
- A reasonably practical guide to validating RFC 9421 HTTP Signatures for ActivityPub in PHP
-----
#WeekInFediverse #Fediverse #ActivityPub
Previous edition: https://mitra.social/objects/01a04a26-eede-72b0-a5e9-f244c3ff2142
Servers
- Ktistec v3.12.0
- Mastodon v4.7.1
- Vernissage v1.43.0
- snac v2.95
- ActivityPub for WordPress v9.3.0
- Owncast v0.3.0
- gathio v1.6.6
- NodeBB v4.15.2
- tootik v0.25.2
- NeoDB v0.18.0
- Bonfire v1.0.7
Clients
- PleromaFE v2.11.4
- Fedilab v3.43.3
- Tuba v0.11.1
- Pixelix v5.2.0
- Summit v1.85.0
- Shoot Web App v1.3.0
Tools and Plugins
- FediFetcher v8.2.0
- Poduptime v6.3.2
- Enable Mastodon Apps v1.6.3 (WordPress plugin)
For developers
- APx v0.27.0
- Library progress report - September 2026 (GoActivityPub)
Articles
- A reasonably practical guide to validating RFC 9421 HTTP Signatures for ActivityPub in PHP
-----
#WeekInFediverse #Fediverse #ActivityPub
Previous edition: https://mitra.social/objects/01a04a26-eede-72b0-a5e9-f244c3ff2142
I have a buncha questions! I will intersperse where relevant.
I am looking at some #indieweb integrations which mix #blog and #ActivityPub - a sort of 1-person masto instance, where responses become comments. Have you looked at any of these?
For me, most of my entries are not public but more journalling, so I am looking for something more complex and secure, yet integrating federated content sharing.
Should publishing with ActivityPub be about as easy as publishing with RSS?
«There is no such thing as full decentralization»
There's always an axis that centralizes: identity, connectivity, discovery, or cost. The useful question isn't "is it decentralized?", it's which of these it sacrificed, and what that costs you the day that point breaks.
https://en.andros.dev/blog/603e4e90/there-is-no-such-thing-as-full-decentralization/
#Web #Fediverso #ActivityPub #XMPP #OrgSocial #IndieWeb #Nostr
andros.dev
A while ago I accidentally reinvented Nostr. Off the back of that article, from comparing the most popular decentralized protocols in detail, one…
«Building a decentralized bulletin board (and accidentally reinventing Nostr)»
Let's build a bulletin board with no account, no sign-up, and no single server behind it. By the time we finish, we'll have accidentally reinvented two technologies that already existed: FidoNet (1984) and Nostr.
https://en.andros.dev/blog/4c4e1b40/building-a-decentralized-bulletin-board-and-accidentally-reinventing-nostr/
#Web #Fediverso #ActivityPub #Algoritmo #IndieWeb #Nostr
andros.dev
Let's do a software engineering exercise. Our goal is to build a bulletin board. A place where anyone can post an ad, a note, a message, with no…
«Building a decentralized bulletin board (and accidentally reinventing Nostr)»
Let's build a bulletin board with no account, no sign-up, and no single server behind it. By the time we finish, we'll have accidentally reinvented two technologies that already existed: FidoNet (1984) and Nostr.
https://en.andros.dev/blog/4c4e1b40/building-a-decentralized-bulletin-board-and-accidentally-reinventing-nostr/
#Web #Fediverso #ActivityPub #Algoritmo #IndieWeb #Nostr
andros.dev
Let's do a software engineering exercise. Our goal is to build a bulletin board. A place where anyone can post an ad, a note, a message, with no…
🆕 blog! “A reasonably practical guide to validating RFC 9421 HTTP Signatures for ActivityPub in PHP”
If you're reading this, you've probably been hitting your head against a brick wall trying to parse and decipher the new HTTP Signatures sent by Mastodon and other Fediverse servers.
This is a…
👀 Read more: https://shkspr.mobi/blog/2026/09/a-reasonably-practical-guide-to-validating-rfc-9421-http-signatures-for-activitypub-in-php/
⸻
#ActivityBot #ActivityPub #mastodon #php #webdev
shkspr.mobi
If you're reading this, you've probably been hitting your head against a brick wall trying to parse and decipher the new HTTP Signatures sent by Mastodon and other Fediverse servers. This is a basic and somewhat incomplete guide to accepting these signatures. I'm sure there are various gotchas, but it works with the signatures I've seen in the wild. Shut Up And Show Me The Code! OK, wow, no…
Woot, a rave review by @thefulcrum 💞 on my blog post "Paradigm shift to the Prosocial web".
https://www.thefulcrum.dev/the-programmers-fulcrum-4-september-2026/
Thank you very much!
#Fediverse today consists of apps and app platforms. It is entirely app-centric.
Since fediverse (luckily) didn't attract much corporate attention yet, it is dominated by #FOSS culture, where people on the #SocialWeb are all "users" that are dependent on FOSS developers to serve their needs. Every dev focuses on their app features first and #interoperability second, leading to a patchwork of poorly integrated apps, and a janky overall experience. The app delivery model is not much different than Web 2.0.
A paradigm shift is possible, to a #SocialNetwork of apps and services, a service-oriented #fedi. The efforts around the #ActivityPub API can be the enabler of this shift.
In this new paradigm devs introduce solutions to a unified #SocialNetworking space, delivering new services to participants on the network. On top of a robust Protocol Layer they model social use cases in the Data Layer, integrating with services and building blocks already available.

coding.social
Only humans make the social web social.
It sure can. I consider #ActivityPub fediverse a social networking environment, and social media a subset of social networking, which I define as: Any direct and indirect human interaction between people.
When you talk of "echo chamber" you refer to a typical anti-pattern of existing social media platforms. It is unfortunate that fediverse grew to be such a microblogging-dominant space, a Twitter++, and it could be said to be a severe lack of imagination on what social networking entails. But it is understandable when we consider how fedi evolves: by post-facto interoperability. Where who comes first decides what goes, and that was Mastodon, a #microblogging app.
Social experience design considers "Personal social networking" where the needs of individual participants are central, and design is inspired by analogies to how we do social offline. #Fediverse becomes a unified space where you navigate communities and move in and out of different social contexts seamlessly.
Based on a recent discussion about the sharedInbox capabilities of #GoActivityPub servers, I had to go back and revisit the logic to make sure it conforms to the spec to the level that I said it does.
Well, after a closer look, I gotta say there were bugs. For example blocked relationships were not checked, so if either the activity actor had blocked one of the sharedInbox recipients, or vice-versa, they still received the activity.
Now this is fixed, but it introduced quite a large number of additional checks in what's already a pretty storage intensive mechanism. I think I need to step back for a moment and reevaluate.
Based on a recent discussion about the sharedInbox capabilities of #GoActivityPub servers, I had to go back and revisit the logic to make sure it conforms to the spec to the level that I said it does.
Well, after a closer look, I gotta say there were bugs. For example blocked relationships were not checked, so if either the activity actor had blocked one of the sharedInbox recipients, or vice-versa, they still received the activity.
Now this is fixed, but it introduced quite a large number of additional checks in what's already a pretty storage intensive mechanism. I think I need to step back for a moment and reevaluate.
It sure can. I consider #ActivityPub fediverse a social networking environment, and social media a subset of social networking, which I define as: Any direct and indirect human interaction between people.
When you talk of "echo chamber" you refer to a typical anti-pattern of existing social media platforms. It is unfortunate that fediverse grew to be such a microblogging-dominant space, a Twitter++, and it could be said to be a severe lack of imagination on what social networking entails. But it is understandable when we consider how fedi evolves: by post-facto interoperability. Where who comes first decides what goes, and that was Mastodon, a #microblogging app.
Social experience design considers "Personal social networking" where the needs of individual participants are central, and design is inspired by analogies to how we do social offline. #Fediverse becomes a unified space where you navigate communities and move in and out of different social contexts seamlessly.
Arguably #fediverse is NOT based on an open standard.
It's based on 'guidance towards an open standard' via the incomplete #ActivityPub spec, sprinkled with what fediverse devs have added over time via post-facto interoperability in what's effectively a Big ball of mud anti-pattern. These extra's are to be discovered on-the-fly, and being app-specific, need to be maintained constantly in another anti-pattern I dubbed Whack-a-mole programming.
We can do better. We can rise above the app-centricness of the fedi, make a paradigm shift that helps increase evolution speed and rate of innovation and sees people's needs be better served..
https://coding.social/blog/prosocial-web
For that to happen major social challenges need to be tackled, like coordinating and collaborating well in a good grassroots standardization process.
I posed the concept of a Grassroots open standard, where services encapsulate their design, and the standardization process is a fedi-native service itself.

coding.social
Social dynamics in the grassroots fediverse ecosystem and laisséz-faire practices led to divergence from power and promise of the ActivityPub protocol. Grassroots standards and the ActivityPub API initiative can get us back on track.
@silverpill @smallcircles
> So far, REST APIs worked very well for fediverse.
This point isn't clear to me. I assume you're referring to the Mastodon API. If so, the #ActivityPub API (including C2S) is a REST API too.
The Mastodon API has worked reasonably well for its intended microblogging use case. However, it's not a governed API and the Mastodon team has made it clear it could change at any time without warning or community involvement.
@silverpill @smallcircles
> So far, REST APIs worked very well for fediverse.
This point isn't clear to me. I assume you're referring to the Mastodon API. If so, the #ActivityPub API (including C2S) is a REST API too.
The Mastodon API has worked reasonably well for its intended microblogging use case. However, it's not a governed API and the Mastodon team has made it clear it could change at any time without warning or community involvement.
🆕 blog! “A reasonably practical guide to validating RFC 9421 HTTP Signatures for ActivityPub in PHP”
If you're reading this, you've probably been hitting your head against a brick wall trying to parse and decipher the new HTTP Signatures sent by Mastodon and other Fediverse servers.
This is a…
👀 Read more: https://shkspr.mobi/blog/2026/09/a-reasonably-practical-guide-to-validating-rfc-9421-http-signatures-for-activitypub-in-php/
⸻
#ActivityBot #ActivityPub #mastodon #php #webdev
shkspr.mobi
If you're reading this, you've probably been hitting your head against a brick wall trying to parse and decipher the new HTTP Signatures sent by Mastodon and other Fediverse servers. This is a basic and somewhat incomplete guide to accepting these signatures. I'm sure there are various gotchas, but it works with the signatures I've seen in the wild. Shut Up And Show Me The Code! OK, wow, no…
Servers
- Hollo v0.9.13
- gush! v0.0.41
- tootik v0.25.0
- Mitra v5.10.0
- Pixelfed v0.12.9
- Hollo v0.9.15
- Funkwhale v2.0.10
- PieFed v1.7.13
- tootik v0.25.1
- NeoDB v0.17.8
Clients
- Pachli v3.8.1
- Fedilab v3.43.2
- RaccoonForFriendica v1.1.0
For developers
Protocol
- FEP-49eb: Batched Inbox Delivery
Articles
- A crawler wanted to know who talks to whom on our server
- The Problems and Successes of Fediverse Comments on my blog
- The Fediverse is Throwing a Music Festival
-----
#WeekInFediverse #Fediverse #ActivityPub
Previous edition: https://mitra.social/objects/01a0269e-d9e5-7a03-a62d-6ef77099e4a7
I just noticed that, if I look up a #Lemmy or #Piefed community on #Mastodon, I'll get a profile page but no posts!
Is that a bug in Mastodon? Or is there no standard #ActivityPub protocol for fetching content from a person/group that you aren't already following?
I seem to recall Mastodon adding a “fetch content from the other server” feature recently, but it sounded like a Mastodon-specific protocol extension was involved. That's…not great. It really should be universal.
I just noticed that, if I look up a #Lemmy or #Piefed community on #Mastodon, I'll get a profile page but no posts!
Is that a bug in Mastodon? Or is there no standard #ActivityPub protocol for fetching content from a person/group that you aren't already following?
I seem to recall Mastodon adding a “fetch content from the other server” feature recently, but it sounded like a Mastodon-specific protocol extension was involved. That's…not great. It really should be universal.
🆕 blog! “A reasonably practical guide to validating RFC 9421 HTTP Signatures for ActivityPub in PHP”
If you're reading this, you've probably been hitting your head against a brick wall trying to parse and decipher the new HTTP Signatures sent by Mastodon and other Fediverse servers.
This is a…
👀 Read more: https://shkspr.mobi/blog/2026/09/a-reasonably-practical-guide-to-validating-rfc-9421-http-signatures-for-activitypub-in-php/
⸻
#ActivityBot #ActivityPub #mastodon #php #webdev
shkspr.mobi
If you're reading this, you've probably been hitting your head against a brick wall trying to parse and decipher the new HTTP Signatures sent by Mastodon and other Fediverse servers. This is a basic and somewhat incomplete guide to accepting these signatures. I'm sure there are various gotchas, but it works with the signatures I've seen in the wild. Shut Up And Show Me The Code! OK, wow, no…
Yes, indeed. Luckily they decided fedi is not interesting enough as yet to either snuff out or capture. If they'd make that decision there'd hardly be a defense.
One of the big weaknesses of fediverse today is that 8 years after #ActivityPub became a W3C Recommendation we are still figuring out basic core protocol plumbing. And without a robust foundation to build compliant solutions on follow-the-leader post-facto interoperability is how the fediverse evolves.
The trend today is that there are two outcomes for fedi's future. Either it doesn't gain broad adoption and remains niche (not the worst outcome), or it "crosses the chasm" and gains broad popularity which will trigger a corporate capture and takeover.
The challenge we face is entirely social in nature. The technology is not the problem. It is there. Open technology space for anyone to dabble in. Fediverse needs a tight-knit community of carers that ensure it will become more than cold technology space.
🆕 blog! “A reasonably practical guide to validating RFC 9421 HTTP Signatures for ActivityPub in PHP”
If you're reading this, you've probably been hitting your head against a brick wall trying to parse and decipher the new HTTP Signatures sent by Mastodon and other Fediverse servers.
This is a…
👀 Read more: https://shkspr.mobi/blog/2026/09/a-reasonably-practical-guide-to-validating-rfc-9421-http-signatures-for-activitypub-in-php/
⸻
#ActivityBot #ActivityPub #mastodon #php #webdev
shkspr.mobi
If you're reading this, you've probably been hitting your head against a brick wall trying to parse and decipher the new HTTP Signatures sent by Mastodon and other Fediverse servers. This is a basic and somewhat incomplete guide to accepting these signatures. I'm sure there are various gotchas, but it works with the signatures I've seen in the wild. Shut Up And Show Me The Code! OK, wow, no…
FYI I created an issue in the #SWICG #ActivityPub API repository relating to my blog post publication..
github.com
I published a blog post at Social coding commons that delves into the opportunities for the ActivityPub API to enable something of a paradigm shift to the fediverse, to what I call the Prosocial we...
🆕 blog! “A reasonably practical guide to validating RFC 9421 HTTP Signatures for ActivityPub in PHP”
If you're reading this, you've probably been hitting your head against a brick wall trying to parse and decipher the new HTTP Signatures sent by Mastodon and other Fediverse servers.
This is a…
👀 Read more: https://shkspr.mobi/blog/2026/09/a-reasonably-practical-guide-to-validating-rfc-9421-http-signatures-for-activitypub-in-php/
⸻
#ActivityBot #ActivityPub #mastodon #php #webdev
shkspr.mobi
If you're reading this, you've probably been hitting your head against a brick wall trying to parse and decipher the new HTTP Signatures sent by Mastodon and other Fediverse servers. This is a basic and somewhat incomplete guide to accepting these signatures. I'm sure there are various gotchas, but it works with the signatures I've seen in the wild. Shut Up And Show Me The Code! OK, wow, no…
Totally. The way #AI wrecks #FOSS culture leads to a lot of soul searching on the merits of the FOSS model we always held dear. FOSS in-the-large may be complicit even to the rise of #BigTech and #LLM, while for individual participants - all working on proper principles and values - it is inherently unsustainable, a burnout factory.
I think AI also opens opportunities. Corporations using it further retracted from directly serving people, leaving empty space, niches for small initiatives to occupy and focus on humaness, real human-to-human connection we crave.
#HCOSS fits perfectly.
Social experience design #SX uses the concept of #SOSS here. See the quoted toot. FOSS software is delivered via Social supply chains, where the needs of all participants in the delivery process are considered.
This concept fits perfectly with the solution-oriented #ActivityPub fediverse of #apps and #services, to offer support. #ForgeFed makes a start on this path.
social.coop
Attached: 1 image @reiver@mastodon.social Yes, I know. And endless strife. SX separates #FOSS from the movement of people that creates it. FOSS is code/software + an open license. Period. Under SX, where solution development involves software creation, FOSS is the end of the supply line. FOSS software is a SX artifact. Its only purpose is to serve the formulated solution that was the reason to start programming. And that solution in turn is only meaningful to the extent it fulfills people's needs. Including the creator, if they need sustenance to continue their good work. This is the crux. It isn't there for FOSS creators, its an inherently unsustainable field. Separating FOSS software from those creating it allows focus on healthy processes, that do not burnout their participants and they leave. Here SX defines #SOSS, Sustainable open social systems.. https://coding.social/blog/reimagine-social/#sustainable-open-social-systems #SX #SocialCoding #SocialWeb
Yes, I know. And endless strife.
SX separates #FOSS from the movement of people that creates it. FOSS is code/software + an open license. Period.
Under SX, where solution development involves software creation, FOSS is the end of the supply line. FOSS software is a SX artifact. Its only purpose is to serve the formulated solution that was the reason to start programming. And that solution in turn is only meaningful to the extent it fulfills people's needs.
Including the creator, if they need sustenance to continue their good work. This is the crux. It isn't there for FOSS creators, its an inherently unsustainable field.
Separating FOSS software from those creating it allows focus on healthy processes, that do not burnout their participants and they leave.
Here SX defines #SOSS, Sustainable open social systems..
https://coding.social/blog/reimagine-social/#sustainable-open-social-systems

Transition from FOSS project to SOSS initiative. Sustainable open social software, to systems, to services, to solutions. Via the Free software development lifecycle (FSDL).

Transition from FOSS project to SOSS initiative. Sustainable open social software, to systems, to services, to solutions. Via the Free software development lifecycle (FSDL).
#Fediverse today consists of apps and app platforms. It is entirely app-centric.
Since fediverse (luckily) didn't attract much corporate attention yet, it is dominated by #FOSS culture, where people on the #SocialWeb are all "users" that are dependent on FOSS developers to serve their needs. Every dev focuses on their app features first and #interoperability second, leading to a patchwork of poorly integrated apps, and a janky overall experience. The app delivery model is not much different than Web 2.0.
A paradigm shift is possible, to a #SocialNetwork of apps and services, a service-oriented #fedi. The efforts around the #ActivityPub API can be the enabler of this shift.
In this new paradigm devs introduce solutions to a unified #SocialNetworking space, delivering new services to participants on the network. On top of a robust Protocol Layer they model social use cases in the Data Layer, integrating with services and building blocks already available.

coding.social
Only humans make the social web social.
Yes, indeed. Luckily they decided fedi is not interesting enough as yet to either snuff out or capture. If they'd make that decision there'd hardly be a defense.
One of the big weaknesses of fediverse today is that 8 years after #ActivityPub became a W3C Recommendation we are still figuring out basic core protocol plumbing. And without a robust foundation to build compliant solutions on follow-the-leader post-facto interoperability is how the fediverse evolves.
The trend today is that there are two outcomes for fedi's future. Either it doesn't gain broad adoption and remains niche (not the worst outcome), or it "crosses the chasm" and gains broad popularity which will trigger a corporate capture and takeover.
The challenge we face is entirely social in nature. The technology is not the problem. It is there. Open technology space for anyone to dabble in. Fediverse needs a tight-knit community of carers that ensure it will become more than cold technology space.
#Fediverse today consists of apps and app platforms. It is entirely app-centric.
Since fediverse (luckily) didn't attract much corporate attention yet, it is dominated by #FOSS culture, where people on the #SocialWeb are all "users" that are dependent on FOSS developers to serve their needs. Every dev focuses on their app features first and #interoperability second, leading to a patchwork of poorly integrated apps, and a janky overall experience. The app delivery model is not much different than Web 2.0.
A paradigm shift is possible, to a #SocialNetwork of apps and services, a service-oriented #fedi. The efforts around the #ActivityPub API can be the enabler of this shift.
In this new paradigm devs introduce solutions to a unified #SocialNetworking space, delivering new services to participants on the network. On top of a robust Protocol Layer they model social use cases in the Data Layer, integrating with services and building blocks already available.

coding.social
Only humans make the social web social.
I argue that #ForgeFed #ActivityPub should support multi-types as specified in #ActivityStreams, even though how to actually work with them in a robust AP extension mechanism hasn't been fully figured out yet.
The argument for avoiding multi-type is to compromise in order to facilitate the current microblogging-heavy installed base on the #fediverse. While that is practical, it will forever keep the tech debt we have created in our fediverse ecosystem.
#Forge federation should not have a primary concern to retain compatility to legacy #microblogging apps, imho.
https://codeberg.org/ForgeFed/ForgeFed/issues/324#issuecomment-22201186
codeberg.org
Hi! I think repositories could work as [FEP-1b12](https://codeberg.org/fediverse/fep/src/branch/main/fep/1b12/fep-1b12.md) or [FEP-171b](https://codeberg.org/fediverse/fep/src/branch/main/fep/171b/fep-171b.md) groups: - Repository is an actor that distributes activities to its followers. The ...
So, I've been running my personal instance of ONI for abut 2 years. I follow about 34 other fediverse accounts (only two on mastodon.social).
It has stored over this time about 3.2GB of data spread over 345K JSON-LD documents (mastodon.social is responsible for about two thirds of the size)
However the thing that's surprising for me the most is that despite the fact that I'm using the least "web scale" storage available - plain files on disk - the instance is still very responsive. The server also does a lot with collection filtering in order to display a meaningful timeline to me, so it's not just a dumb pipe from disk to browser.
So, I've been running my personal instance of ONI for abut 2 years. I follow about 34 other fediverse accounts (only two on mastodon.social).
It has stored over this time about 3.2GB of data spread over 345K JSON-LD documents (mastodon.social is responsible for about two thirds of the size)
However the thing that's surprising for me the most is that despite the fact that I'm using the least "web scale" storage available - plain files on disk - the instance is still very responsive. The server also does a lot with collection filtering in order to display a meaningful timeline to me, so it's not just a dumb pipe from disk to browser.
Verehrtes #Fediverse –
die #ActivityPub #ClientToServer API ist magisch.
Für menschy setzen wir momentan die neuen Spezifikationen um.
Zum Beispiel
Auth
https://
swicg.github.io/activitypub-api/basicprofile
with according
Metadata https://
client.dev/
and example in https://
github.com/swicg/activitypub-api
Seek
https://
swicg.github.io/activitypub-api/seekitem
Autocomplete
https://
swicg.github.io/activitypub-api/autocomplete
Server Sent Events
https://
swicg.github.io/activitypub-api/sse
HTML Discovery
https://swicg.github.io/activitypub-html-discovery/
swicg.github.io
I argue that #ForgeFed #ActivityPub should support multi-types as specified in #ActivityStreams, even though how to actually work with them in a robust AP extension mechanism hasn't been fully figured out yet.
The argument for avoiding multi-type is to compromise in order to facilitate the current microblogging-heavy installed base on the #fediverse. While that is practical, it will forever keep the tech debt we have created in our fediverse ecosystem.
#Forge federation should not have a primary concern to retain compatility to legacy #microblogging apps, imho.
https://codeberg.org/ForgeFed/ForgeFed/issues/324#issuecomment-22201186
codeberg.org
Hi! I think repositories could work as [FEP-1b12](https://codeberg.org/fediverse/fep/src/branch/main/fep/1b12/fep-1b12.md) or [FEP-171b](https://codeberg.org/fediverse/fep/src/branch/main/fep/171b/fep-171b.md) groups: - Repository is an actor that distributes activities to its followers. The ...
#FediHood now supports #Lemmy through #ActivityPub. You can subscribe/unsubscribe from groups, read threaded discussions with their upvote scores, and reply or upvote yourself (through the fav button).
This is the first step: FediHood will also automatically split messages into city, region and country groups that everyone on the fediverse using groups will be able to interact with.
This release encompasses two broad sets of changes: widening the pool of candidates for feeds and more carefully checking the origin of inbound activities (broadly FEP-fe34, Origin-based security model).
Candidates for feeds were originally limited to posts in the actor's mailboxes. This decision made authorization easy but omitted clearly acceptable posts (for example, posts addressed to the public collection) that arrived via other means (for example, filling in a thread).
Prior to this release, inbox processing did not consistently define an object's origin nor did it apply consistent rules to what it admitted based on the origin.
FEP-fe34 is still not fully enforced, and truth be told, I'm still reviewing some of its mandates, so I'm not yet listing it in Ktistec's federation documentation.
Here's the full list of changes:
Added
Fixed
Create and Update to update objects.Changed
Removed
proxyUrl property from the actor document.I am still working hard on the feed deck. Both notifications and feed ordering now work. It's now my preferred reading interface, and should be ready by the next release!
epiktistes.com
The culmination of my recent work on algorithmic feeds is a #fediverse feed reading machine for all of the topics I care about. #ktistec
#FediHood now supports #Lemmy through #ActivityPub. You can subscribe/unsubscribe from groups, read threaded discussions with their upvote scores, and reply or upvote yourself (through the fav button).
This is the first step: FediHood will also automatically split messages into city, region and country groups that everyone on the fediverse using groups will be able to interact with.
This release encompasses two broad sets of changes: widening the pool of candidates for feeds and more carefully checking the origin of inbound activities (broadly FEP-fe34, Origin-based security model).
Candidates for feeds were originally limited to posts in the actor's mailboxes. This decision made authorization easy but omitted clearly acceptable posts (for example, posts addressed to the public collection) that arrived via other means (for example, filling in a thread).
Prior to this release, inbox processing did not consistently define an object's origin nor did it apply consistent rules to what it admitted based on the origin.
FEP-fe34 is still not fully enforced, and truth be told, I'm still reviewing some of its mandates, so I'm not yet listing it in Ktistec's federation documentation.
Here's the full list of changes:
Added
Fixed
Create and Update to update objects.Changed
Removed
proxyUrl property from the actor document.I am still working hard on the feed deck. Both notifications and feed ordering now work. It's now my preferred reading interface, and should be ready by the next release!
epiktistes.com
The culmination of my recent work on algorithmic feeds is a #fediverse feed reading machine for all of the topics I care about. #ktistec
#FediHood now supports #Lemmy through #ActivityPub. You can subscribe/unsubscribe from groups, read threaded discussions with their upvote scores, and reply or upvote yourself (through the fav button).
This is the first step: FediHood will also automatically split messages into city, region and country groups that everyone on the fediverse using groups will be able to interact with.
Is "Quiet Public" a #GotoSocial thing, or is it #ActivityPub? #tech #mastodon
Self hosted trail mapping and GPS tracking with Wanderer
I deployed an instance of Wanderer and defined a route in preparation for my upcoming trip to The Great Smokey Mountains National Park next week. This service uses #ActivityPub but I haven't been able to follow my account from #Mastodon yet... Even so I'm glad to add this to my #selfhosted arsenal. I will be live streaming on #Owncast during my hike, and I'll also edit the footage for #PeerTube.
wanderer.eom.dev
<p>A six-day hiking trip through The Great Smokey Mountains National Park</p>
🆕 blog! “Book Review: ActivityPub by Evan Prodromou”
★★★★⯪
As part of my grant from NLnet to improve my Fediverse bot project, I'm spending time making sure I understand all the fundamentals of ActivityPub. The problem with Internet standards is they're scattered all over the place. Abandoned forums, half-maintained wikis, mailing lists with obscure rules, and…
👀 Read more: https://shkspr.mobi/blog/2026/09/book-review-activitypub-by-evan-prodromou/
⸻
#ActivityPub #BookReview
shkspr.mobi
As part of my grant from NLnet to improve my Fediverse bot project, I'm spending time making sure I understand all the fundamentals of ActivityPub. The problem with Internet standards is they're scattered all over the place. Abandoned forums, half-maintained wikis, mailing lists with obscure rules, and monolithic blocks of text. Sometimes you just want one book which collates all the info and…
Self hosted trail mapping and GPS tracking with Wanderer
I deployed an instance of Wanderer and defined a route in preparation for my upcoming trip to The Great Smokey Mountains National Park next week. This service uses #ActivityPub but I haven't been able to follow my account from #Mastodon yet... Even so I'm glad to add this to my #selfhosted arsenal. I will be live streaming on #Owncast during my hike, and I'll also edit the footage for #PeerTube.
wanderer.eom.dev
<p>A six-day hiking trip through The Great Smokey Mountains National Park</p>
In a new white paper we develop graph representations of decentralized (heterogeneous) Social Web networks. We build primarily on the machinery of multilayer networks with their associated tensor representations.
We expand on the specialized subclass of so-called layer-disjoint networks as they offer powerful tools for modeling complex online interactions of Actors within distinct Communities.
1/
#activitypub
#atproto
#decentralization
#socialweb
https://www.openriskmanagement.com/towards_graph_calculus_for_decentralized_social_networks/

Visual representation of layer-disjoint multilayer framework that models a decentralized Social Web framework where Actors form distinct interacting Communities. Four layers are presented (four communities) as overlaping grids. Actors are represented as colored dots. Connections between Actors are directed edges (arrows).
In a new white paper we develop graph representations of decentralized (heterogeneous) Social Web networks. We build primarily on the machinery of multilayer networks with their associated tensor representations.
We expand on the specialized subclass of so-called layer-disjoint networks as they offer powerful tools for modeling complex online interactions of Actors within distinct Communities.
1/
#activitypub
#atproto
#decentralization
#socialweb
https://www.openriskmanagement.com/towards_graph_calculus_for_decentralized_social_networks/

Visual representation of layer-disjoint multilayer framework that models a decentralized Social Web framework where Actors form distinct interacting Communities. Four layers are presented (four communities) as overlaping grids. Actors are represented as colored dots. Connections between Actors are directed edges (arrows).
Can someone that's familiar with the details of FEP-044f (the one underlying Mastodon's quote posts) tell me if there's something I missed and there *IS* a way to ensure that malicious instances (or clients) aren't able to disregard the whole thing and just allow and present quoted posts without their original user's consent?
🆕 blog! “Book Review: ActivityPub by Evan Prodromou”
★★★★⯪
As part of my grant from NLnet to improve my Fediverse bot project, I'm spending time making sure I understand all the fundamentals of ActivityPub. The problem with Internet standards is they're scattered all over the place. Abandoned forums, half-maintained wikis, mailing lists with obscure rules, and…
👀 Read more: https://shkspr.mobi/blog/2026/09/book-review-activitypub-by-evan-prodromou/
⸻
#ActivityPub #BookReview
shkspr.mobi
As part of my grant from NLnet to improve my Fediverse bot project, I'm spending time making sure I understand all the fundamentals of ActivityPub. The problem with Internet standards is they're scattered all over the place. Abandoned forums, half-maintained wikis, mailing lists with obscure rules, and monolithic blocks of text. Sometimes you just want one book which collates all the info and…
Can someone that's familiar with the details of FEP-044f (the one underlying Mastodon's quote posts) tell me if there's something I missed and there *IS* a way to ensure that malicious instances (or clients) aren't able to disregard the whole thing and just allow and present quoted posts without their original user's consent?
🆕 blog! “Book Review: ActivityPub by Evan Prodromou”
★★★★⯪
As part of my grant from NLnet to improve my Fediverse bot project, I'm spending time making sure I understand all the fundamentals of ActivityPub. The problem with Internet standards is they're scattered all over the place. Abandoned forums, half-maintained wikis, mailing lists with obscure rules, and…
👀 Read more: https://shkspr.mobi/blog/2026/09/book-review-activitypub-by-evan-prodromou/
⸻
#ActivityPub #BookReview
shkspr.mobi
As part of my grant from NLnet to improve my Fediverse bot project, I'm spending time making sure I understand all the fundamentals of ActivityPub. The problem with Internet standards is they're scattered all over the place. Abandoned forums, half-maintained wikis, mailing lists with obscure rules, and monolithic blocks of text. Sometimes you just want one book which collates all the info and…
A weird #ActivityPub message from #Frendica.
Signed on 2026-09-01
Published on 2026-03-02
That's a skew of six months! The message type is "Undo" - so they're undoing a like they sent in March.
Is there *really* a worry about accepting requests like this? Given the message has been signed, what risk is there to replay attacks?
Bug report at https://github.com/friendica/friendica/issues/16150
github.com
I have searched open and closed issues for duplicates Bug Description My AP server has received an Undo request from a server running Friendica 'Blutwurz' 2026.05-1595 The header which contains the...
A weird #ActivityPub message from #Frendica.
Signed on 2026-09-01
Published on 2026-03-02
That's a skew of six months! The message type is "Undo" - so they're undoing a like they sent in March.
Is there *really* a worry about accepting requests like this? Given the message has been signed, what risk is there to replay attacks?
Bug report at https://github.com/friendica/friendica/issues/16150
github.com
I have searched open and closed issues for duplicates Bug Description My AP server has received an Undo request from a server running Friendica 'Blutwurz' 2026.05-1595 The header which contains the...
I migrated from my self-hosted Mastodon instance to mastodon.social. Of course, I couldn't move my posts so I created a small server to provide the archive of my post history without changing any #ActivityPub URIs or media URLs. It seems to be working well. RAM usage is down from 1.2GB to 45MB (-97%) and disk usage down from 160GB to 15MB (-99.99% drop) compared to keeping the old server running. I've been surprised how many "hits" I'm seeing for old posts, so I'm glad the archive is available.
Our project website is live now! 🐇
Learn about federated blogging at: https://join.omicron.blog/
#blog #blogpost #blogging #fediverse #Omicron #internet #activityPub #mastodon
join.omicron.blog
Omicron is a free and open federated blogging platform that belongs to everyone. Write freely, publish to the fediverse, and own your words.
Our project website is live now! 🐇
Learn about federated blogging at: https://join.omicron.blog/
#blog #blogpost #blogging #fediverse #Omicron #internet #activityPub #mastodon
join.omicron.blog
Omicron is a free and open federated blogging platform that belongs to everyone. Write freely, publish to the fediverse, and own your words.
Hey, #Mastodon and #ActivityPub developers.
How much skew do you allow before rejecting a message?
I've just received something where the header is signed:
Mon, 31 Aug 2026 20:09:54 GMT
But the ActivityPub message was published:
Mon, 31 Aug 2026 19:58:36 GMT
That's a little over 10 minutes. Is that too much? Should I not care as long as the signature validates?
Hey, #Mastodon and #ActivityPub developers.
How much skew do you allow before rejecting a message?
I've just received something where the header is signed:
Mon, 31 Aug 2026 20:09:54 GMT
But the ActivityPub message was published:
Mon, 31 Aug 2026 19:58:36 GMT
That's a little over 10 minutes. Is that too much? Should I not care as long as the signature validates?
Esteemed #Fediverse –
the #ActivityPub #ClientToServer API is magic.
For menschys we are currently implementing the new specs.
There are multiple drafts already, e.g.
Auth
https://swicg.github.io/activitypub-api/basicprofile
with according
Metadata https://client.dev/
and example in https://github.com/swicg/activitypub-api
Seek
https://swicg.github.io/activitypub-api/seekitem
Autocomplete
https://swicg.github.io/activitypub-api/autocomplete
Server Sent Events
https://swicg.github.io/activitypub-api/sse
HTML Discovery
https://swicg.github.io/activitypub-html-discovery/
swicg.github.io
Hey there,
this is about the new #ActivityPub C2S specs. introducing new properties.
In https://swicg.github.io/activitypub-api/basicprofile#discovery – the according example https://swicg.github.io/activitypub-api/examples/oauth/index.html expects the properties to be either in the /.well-known/oauth-authorization-server or in the Actor.
How can I support them in Actors created by fedify, e.g. in an ActorDispatcher ?
Maybe they should be defined in the default context …
#fedify #oauth
swicg.github.io
🆕 blog! “ActivityBot is the recipient of an NLnet grant!”
Back in February, I applied for NLnet's Next Generation Zero grant. They were looking for Fediverse projects to help rewild the social media landscape. Or, as they describe it:
Reclaim the public nature of the internet
Small and medium-sized R&D grants between 5.000 and 50.000 euro,…
👀 Read more: https://shkspr.mobi/blog/2026/08/activitybot-is-the-recipient-of-an-nlnet-grant/
⸻
#ActivityBot #ActivityPub #fediverse #NLnet
shkspr.mobi
Back in February, I applied for NLnet's Next Generation Zero grant. They were looking for Fediverse projects to help rewild the social media landscape. Or, as they describe it: Reclaim the public nature of the internet Small and medium-sized R&D grants between 5.000 and 50.000 euro, with the possibility to scale up. I run ActivityBot - it is a single-file ActivityPub server suitable for…
«There is no such thing as full decentralization»
There's always an axis that centralizes: identity, connectivity, discovery, or cost. The useful question isn't "is it decentralized?", it's which of these it sacrificed, and what that costs you the day that point breaks.
https://en.andros.dev/blog/603e4e90/there-is-no-such-thing-as-full-decentralization/
#Web #Fediverso #ActivityPub #XMPP #OrgSocial #IndieWeb #Nostr
andros.dev
A while ago I accidentally reinvented Nostr. Off the back of that article, from comparing the most popular decentralized protocols in detail, one…
«There is no such thing as full decentralization»
There's always an axis that centralizes: identity, connectivity, discovery, or cost. The useful question isn't "is it decentralized?", it's which of these it sacrificed, and what that costs you the day that point breaks.
https://en.andros.dev/blog/603e4e90/there-is-no-such-thing-as-full-decentralization/
#Web #Fediverso #ActivityPub #XMPP #OrgSocial #IndieWeb #Nostr
andros.dev
A while ago I accidentally reinvented Nostr. Off the back of that article, from comparing the most popular decentralized protocols in detail, one…
Just had a guy name that one famous guy that invented activity pub. I have a strong feeling that it was a group of people and some flinta in it.
So please let me know of all the great people who made our communication here possible.
Wikipedia was only partly helpful in my quest to find the people who collaborated.
Just had a guy name that one famous guy that invented activity pub. I have a strong feeling that it was a group of people and some flinta in it.
So please let me know of all the great people who made our communication here possible.
Wikipedia was only partly helpful in my quest to find the people who collaborated.
I have migrated my ActivityPub and Mastodon-compatible bots to use a queue and parallel workers. Synchronized requests were taking too long for many bots with many followers.
Upgrade now if you use my bot software:
https://github.com/nekromoff/mastodon-bots
#bot #bots #mastobots #mastoadmin #mastolivre #fediverse #activityPub #software #webdev #tools #webdevelopment
github.com
Autopost on Fediverse! ActivityPub / Mastodon compatible instance to quickly create and deploy multiple bots (identities) that can be controlled via simple API. - nekromoff/mastodon-bots
I have migrated my ActivityPub and Mastodon-compatible bots to use a queue and parallel workers. Synchronized requests were taking too long for many bots with many followers.
Upgrade now if you use my bot software:
https://github.com/nekromoff/mastodon-bots
#bot #bots #mastobots #mastoadmin #mastolivre #fediverse #activityPub #software #webdev #tools #webdevelopment
github.com
Autopost on Fediverse! ActivityPub / Mastodon compatible instance to quickly create and deploy multiple bots (identities) that can be controlled via simple API. - nekromoff/mastodon-bots
🆕 blog! “ActivityBot is the recipient of an NLnet grant!”
Back in February, I applied for NLnet's Next Generation Zero grant. They were looking for Fediverse projects to help rewild the social media landscape. Or, as they describe it:
Reclaim the public nature of the internet
Small and medium-sized R&D grants between 5.000 and 50.000 euro,…
👀 Read more: https://shkspr.mobi/blog/2026/08/activitybot-is-the-recipient-of-an-nlnet-grant/
⸻
#ActivityBot #ActivityPub #fediverse #NLnet
shkspr.mobi
Back in February, I applied for NLnet's Next Generation Zero grant. They were looking for Fediverse projects to help rewild the social media landscape. Or, as they describe it: Reclaim the public nature of the internet Small and medium-sized R&D grants between 5.000 and 50.000 euro, with the possibility to scale up. I run ActivityBot - it is a single-file ActivityPub server suitable for…
«Building a decentralized bulletin board (and accidentally reinventing Nostr)»
Let's build a bulletin board with no account, no sign-up, and no single server behind it. By the time we finish, we'll have accidentally reinvented two technologies that already existed: FidoNet (1984) and Nostr.
https://en.andros.dev/blog/4c4e1b40/building-a-decentralized-bulletin-board-and-accidentally-reinventing-nostr/
#Web #Fediverso #ActivityPub #Algoritmo #IndieWeb #Nostr
andros.dev
Let's do a software engineering exercise. Our goal is to build a bulletin board. A place where anyone can post an ad, a note, a message, with no…
🆕 blog! “ActivityBot is the recipient of an NLnet grant!”
Back in February, I applied for NLnet's Next Generation Zero grant. They were looking for Fediverse projects to help rewild the social media landscape. Or, as they describe it:
Reclaim the public nature of the internet
Small and medium-sized R&D grants between 5.000 and 50.000 euro,…
👀 Read more: https://shkspr.mobi/blog/2026/08/activitybot-is-the-recipient-of-an-nlnet-grant/
⸻
#ActivityBot #ActivityPub #fediverse #NLnet
shkspr.mobi
Back in February, I applied for NLnet's Next Generation Zero grant. They were looking for Fediverse projects to help rewild the social media landscape. Or, as they describe it: Reclaim the public nature of the internet Small and medium-sized R&D grants between 5.000 and 50.000 euro, with the possibility to scale up. I run ActivityBot - it is a single-file ActivityPub server suitable for…
🆕 blog! “ActivityBot is the recipient of an NLnet grant!”
Back in February, I applied for NLnet's Next Generation Zero grant. They were looking for Fediverse projects to help rewild the social media landscape. Or, as they describe it:
Reclaim the public nature of the internet
Small and medium-sized R&D grants between 5.000 and 50.000 euro,…
👀 Read more: https://shkspr.mobi/blog/2026/08/activitybot-is-the-recipient-of-an-nlnet-grant/
⸻
#ActivityBot #ActivityPub #fediverse #NLnet
shkspr.mobi
Back in February, I applied for NLnet's Next Generation Zero grant. They were looking for Fediverse projects to help rewild the social media landscape. Or, as they describe it: Reclaim the public nature of the internet Small and medium-sized R&D grants between 5.000 and 50.000 euro, with the possibility to scale up. I run ActivityBot - it is a single-file ActivityPub server suitable for…
🆕 blog! “ActivityBot is the recipient of an NLnet grant!”
Back in February, I applied for NLnet's Next Generation Zero grant. They were looking for Fediverse projects to help rewild the social media landscape. Or, as they describe it:
Reclaim the public nature of the internet
Small and medium-sized R&D grants between 5.000 and 50.000 euro,…
👀 Read more: https://shkspr.mobi/blog/2026/08/activitybot-is-the-recipient-of-an-nlnet-grant/
⸻
#ActivityBot #ActivityPub #fediverse #NLnet
shkspr.mobi
Back in February, I applied for NLnet's Next Generation Zero grant. They were looking for Fediverse projects to help rewild the social media landscape. Or, as they describe it: Reclaim the public nature of the internet Small and medium-sized R&D grants between 5.000 and 50.000 euro, with the possibility to scale up. I run ActivityBot - it is a single-file ActivityPub server suitable for…
«Building a decentralized bulletin board (and accidentally reinventing Nostr)»
Let's build a bulletin board with no account, no sign-up, and no single server behind it. By the time we finish, we'll have accidentally reinvented two technologies that already existed: FidoNet (1984) and Nostr.
https://en.andros.dev/blog/4c4e1b40/building-a-decentralized-bulletin-board-and-accidentally-reinventing-nostr/
#Web #Fediverso #ActivityPub #Algoritmo #IndieWeb #Nostr
andros.dev
Let's do a software engineering exercise. Our goal is to build a bulletin board. A place where anyone can post an ad, a note, a message, with no…
«Building a decentralized bulletin board (and accidentally reinventing Nostr)»
Let's build a bulletin board with no account, no sign-up, and no single server behind it. By the time we finish, we'll have accidentally reinvented two technologies that already existed: FidoNet (1984) and Nostr.
https://en.andros.dev/blog/4c4e1b40/building-a-decentralized-bulletin-board-and-accidentally-reinventing-nostr/
#Web #Fediverso #ActivityPub #Algoritmo #IndieWeb #Nostr
andros.dev
Let's do a software engineering exercise. Our goal is to build a bulletin board. A place where anyone can post an ad, a note, a message, with no…
Is there a way to fetch a profile's JSON from a server with authorized_fetch enabled without being a fully fledged actor? It doesn't feel like what I'm trying to do is bad, I want to give people the option to get information about themselves from their profiles, as is visible to the world in the HTML view, but it seems like authorized_fetch is expecting a lot more than I thought.
I guess I might need to get them to actually log in, but am I right in thinking there that that's specifically a Mastodon API (that other servers may have implemented for compatibility)? It would be nice to be compatible with everything, but it does feel like I'm fighting the protocol at every step here.
So, um, Tusky on Android let me make a Mastodon bio longer than 500 characters.
On the standard Mastodon web client, the UI is shouting at me that I can't edit it unless I reduce it to 500 chars.
I am guessing ActivityPub has no specific limit on a bio for a user, but can anyone tell me the risk/reward (and/or pros/cons) of making my bio > 500 chars?
Like, will some clients/federated servers truncate at 500 chars?
Is there a way to fetch a profile's JSON from a server with authorized_fetch enabled without being a fully fledged actor? It doesn't feel like what I'm trying to do is bad, I want to give people the option to get information about themselves from their profiles, as is visible to the world in the HTML view, but it seems like authorized_fetch is expecting a lot more than I thought.
I guess I might need to get them to actually log in, but am I right in thinking there that that's specifically a Mastodon API (that other servers may have implemented for compatibility)? It would be nice to be compatible with everything, but it does feel like I'm fighting the protocol at every step here.
I recently discovered that Jorge Aguilera (@jorge) gave a 1 hour introduction to #ActivityPub in Spanish called "ActivityPub: El Protocolo que Impulsa el Fediverso" at Commit Conf in Madrid and the whole thing was recorded:
https://koliseo.com/commit/2026/agenda/1?selected=JNIEz5zgefpMwrJMRpl7 #fedidev #fediverso
I recently discovered that Jorge Aguilera (@jorge) gave a 1 hour introduction to #ActivityPub in Spanish called "ActivityPub: El Protocolo que Impulsa el Fediverso" at Commit Conf in Madrid and the whole thing was recorded:
https://koliseo.com/commit/2026/agenda/1?selected=JNIEz5zgefpMwrJMRpl7 #fedidev #fediverso
So, um, Tusky on Android let me make a Mastodon bio longer than 500 characters.
On the standard Mastodon web client, the UI is shouting at me that I can't edit it unless I reduce it to 500 chars.
I am guessing ActivityPub has no specific limit on a bio for a user, but can anyone tell me the risk/reward (and/or pros/cons) of making my bio > 500 chars?
Like, will some clients/federated servers truncate at 500 chars?
Servers
- Hollo v0.9.13
- gush! v0.0.41
- tootik v0.25.0
- Mitra v5.10.0
- Pixelfed v0.12.9
- Hollo v0.9.15
- Funkwhale v2.0.10
- PieFed v1.7.13
- tootik v0.25.1
- NeoDB v0.17.8
Clients
- Pachli v3.8.1
- Fedilab v3.43.2
- RaccoonForFriendica v1.1.0
For developers
Protocol
- FEP-49eb: Batched Inbox Delivery
Articles
- A crawler wanted to know who talks to whom on our server
- The Problems and Successes of Fediverse Comments on my blog
- The Fediverse is Throwing a Music Festival
-----
#WeekInFediverse #Fediverse #ActivityPub
Previous edition: https://mitra.social/objects/01a0269e-d9e5-7a03-a62d-6ef77099e4a7
Servers
- Hollo v0.9.13
- gush! v0.0.41
- tootik v0.25.0
- Mitra v5.10.0
- Pixelfed v0.12.9
- Hollo v0.9.15
- Funkwhale v2.0.10
- PieFed v1.7.13
- tootik v0.25.1
- NeoDB v0.17.8
Clients
- Pachli v3.8.1
- Fedilab v3.43.2
- RaccoonForFriendica v1.1.0
For developers
Protocol
- FEP-49eb: Batched Inbox Delivery
Articles
- A crawler wanted to know who talks to whom on our server
- The Problems and Successes of Fediverse Comments on my blog
- The Fediverse is Throwing a Music Festival
-----
#WeekInFediverse #Fediverse #ActivityPub
Previous edition: https://mitra.social/objects/01a0269e-d9e5-7a03-a62d-6ef77099e4a7
If you know anyone working on an ActivityPub project and they need any advice/feedback/help or want to spread the word, tell them to DM me! I am more than willing to help or spread awareness! #activitypub #fediverse
Somewhere else, this is a patent fight. Here it's a GitHub issue.
https://github.com/pixelfed/starter-kits/issues/1
Almost 2 years ago I started planning federated Starter Kits. @Mastodon took the idea, turned it into a FEP, and built it with my feedback rather than around it.
It shipped in @loops this year. Mastodon shipped Featured Collections months later.
Compatible. Collaborative. Consent-driven 🚀
https://blog.joinloops.org/introducing-starter-kits/
https://blog.joinmastodon.org/2026/06/mastodon-4.6/
#StarterKits #FeaturedCollections #ActivityPub #Collaboration
blog.joinmastodon.org
In Mastodon 4.6, we are introducing a way to create and share curated collections of profiles. We've also reworked profiles and the profile editing experience, added some institutional features, and fixed many accessibility issues.
Name another photo app that will happily send you to a competitor.
If Pixelfed isn't for you, there are many amazing alternatives like:
- @frequency
- @gotosocial
- @admin
- @vernissage
All of them let you share images. All of them talk to Pixelfed. Leaving isn't a punishment here, because you never lose the network.
When you put people first, you build doors instead of cages.
🆕 blog! “Some updates to ActivityBot”
I couple of years ago, I developed ActivityBot - the simplest way to build Mastodon Bots. It is a single PHP file which can run an entire ActivityPub server and it is less than 80KB.
It works! You can follow @openbenches to see the latest entries on OpenBenches.org, and @colours for a …
👀 Read more: https://shkspr.mobi/blog/2026/03/some-updates-to-activitybot/
⸻
#ActivityBot #ActivityPub #mastodon #php
shkspr.mobi
I couple of years ago, I developed ActivityBot - the simplest way to build Mastodon Bots. It is a single PHP file which can run an entire ActivityPub server and it is less than 80KB. It works! You can follow @openbenches@bot.openbenches.org to see the latest entries on OpenBenches.org, and @colours@colours.bots.edent.tel for a slice of colour in your day, and @solar@solar.bots.edent.tel to see…
Name another photo app that will happily send you to a competitor.
If Pixelfed isn't for you, there are many amazing alternatives like:
- @frequency
- @gotosocial
- @admin
- @vernissage
All of them let you share images. All of them talk to Pixelfed. Leaving isn't a punishment here, because you never lose the network.
When you put people first, you build doors instead of cages.
Name another photo app that will happily send you to a competitor.
If Pixelfed isn't for you, there are many amazing alternatives like:
- @frequency
- @gotosocial
- @admin
- @vernissage
All of them let you share images. All of them talk to Pixelfed. Leaving isn't a punishment here, because you never lose the network.
When you put people first, you build doors instead of cages.
Servers
- Hollo v0.9.13
- gush! v0.0.41
- tootik v0.25.0
- Mitra v5.10.0
- Pixelfed v0.12.9
- Hollo v0.9.15
- Funkwhale v2.0.10
- PieFed v1.7.13
- tootik v0.25.1
- NeoDB v0.17.8
Clients
- Pachli v3.8.1
- Fedilab v3.43.2
- RaccoonForFriendica v1.1.0
For developers
Protocol
- FEP-49eb: Batched Inbox Delivery
Articles
- A crawler wanted to know who talks to whom on our server
- The Problems and Successes of Fediverse Comments on my blog
- The Fediverse is Throwing a Music Festival
-----
#WeekInFediverse #Fediverse #ActivityPub
Previous edition: https://mitra.social/objects/01a0269e-d9e5-7a03-a62d-6ef77099e4a7
Servers
- Hollo v0.9.13
- gush! v0.0.41
- tootik v0.25.0
- Mitra v5.10.0
- Pixelfed v0.12.9
- Hollo v0.9.15
- Funkwhale v2.0.10
- PieFed v1.7.13
- tootik v0.25.1
- NeoDB v0.17.8
Clients
- Pachli v3.8.1
- Fedilab v3.43.2
- RaccoonForFriendica v1.1.0
For developers
Protocol
- FEP-49eb: Batched Inbox Delivery
Articles
- A crawler wanted to know who talks to whom on our server
- The Problems and Successes of Fediverse Comments on my blog
- The Fediverse is Throwing a Music Festival
-----
#WeekInFediverse #Fediverse #ActivityPub
Previous edition: https://mitra.social/objects/01a0269e-d9e5-7a03-a62d-6ef77099e4a7
What are the most important issues and discussions about the Open Social Web we need to have this late summer and fall?
#FediForum #FediForumFriday #ActivityPub #ATProtocol #Fediverse #atproto
What are the most important issues and discussions about the Open Social Web we need to have this late summer and fall?
#FediForum #FediForumFriday #ActivityPub #ATProtocol #Fediverse #atproto
MOBILIZE WITHOUT MONOPOLIES: Federated Event Organizing Beyond Big Tech — Mobilizon — E97
Special thank you to @mobilizon@framapiaf.org
🚀 #VOD tubefree.org/w/dgzcoWWwtaCqt...
Never miss an episode!
🔎 #Follow @ozoned@tubefree.org
📡 #RSS tubefree.org/feeds/videos.xml?videoChannelId=5934
🎧 #Podcast tubefree.org/feeds/podcast/videos.xml?videoChannelId=5934 #FiresideFedi #FiresideChat #Podcast #VideoPodcast #Fediverse #Fedi #ActivityPub #DecentralizedSocial #OpenSocialWeb #Community #VodCast
btfree.social
BT Free is a non-profit organization founded by @ozoned@btfree.social . It's goal is for digital privacy rights, advocacy and consulting. This goal will be attained by hosting open platforms to allow
MOBILIZE WITHOUT MONOPOLIES: Federated Event Organizing Beyond Big Tech — Mobilizon — E97
Special thank you to @mobilizon@framapiaf.org
🚀 #VOD tubefree.org/w/dgzcoWWwtaCqt...
Never miss an episode!
🔎 #Follow @ozoned@tubefree.org
📡 #RSS tubefree.org/feeds/videos.xml?videoChannelId=5934
🎧 #Podcast tubefree.org/feeds/podcast/videos.xml?videoChannelId=5934 #FiresideFedi #FiresideChat #Podcast #VideoPodcast #Fediverse #Fedi #ActivityPub #DecentralizedSocial #OpenSocialWeb #Community #VodCast
btfree.social
BT Free is a non-profit organization founded by @ozoned@btfree.social . It's goal is for digital privacy rights, advocacy and consulting. This goal will be attained by hosting open platforms to allow
ActivityPub defines the social graph. It leaves institutional authority undefined - a verification badge, a moderator's standing, one instance's trust in another are all unsigned platform state. No third party can check any of it.
NOMOI is an open protocol for verifiable delegation of authority, and it needs nothing exotic here: every actor already has a signing key.
Site's live 👉 https://nomoi-labs.com
Spec + Go client, Apache-2.0, on Codeberg.
nomoi-labs.com
NOMOI records delegated authority as a chain of signed, immutable events that anyone can verify without calling the issuer.
It looks like the migration to this new instance was [mostly] successful. However it does appear I lost some followers and followees.
Note to fediverse app and ActivityPub maintainers: we really need one and only one schema and format, based on flat text lines or .csv, for importing, exporting, migrating profiles, posts, follow(ing) lists, etc. Too many cooks spoils the stew.
It looks like the migration to this new instance was [mostly] successful. However it does appear I lost some followers and followees.
Note to fediverse app and ActivityPub maintainers: we really need one and only one schema and format, based on flat text lines or .csv, for importing, exporting, migrating profiles, posts, follow(ing) lists, etc. Too many cooks spoils the stew.
One of the funny things about reading the #ActivityPub specification is how much @cwebber loves #Emacs #orgmode.
In our read-through tonight we read the aside that said it might be a good idea to abandon editing a Note if it could mess up your org-mode formatting, and I was like, yeah, that sounds like something Christine would do.
One of the funny things about reading the #ActivityPub specification is how much @cwebber loves #Emacs #orgmode.
In our read-through tonight we read the aside that said it might be a good idea to abandon editing a Note if it could mess up your org-mode formatting, and I was like, yeah, that sounds like something Christine would do.
ActivityPub defines the social graph. It leaves institutional authority undefined - a verification badge, a moderator's standing, one instance's trust in another are all unsigned platform state. No third party can check any of it.
NOMOI is an open protocol for verifiable delegation of authority, and it needs nothing exotic here: every actor already has a signing key.
Site's live 👉 https://nomoi-labs.com
Spec + Go client, Apache-2.0, on Codeberg.
nomoi-labs.com
NOMOI records delegated authority as a chain of signed, immutable events that anyone can verify without calling the issuer.
@hongminhee how can we get more East Asian #ActivityPub developers involved in the #W3C #SocialCG and #SocialWG?
We run social.vir.group - a Mastodon instance on the ActivityPub federated protocol.
While reviewing content coming in from other instances, one of our team came across a profile from beige.party with an unusual field set:
Field name: Anthropic sod off
Field value: ANTHROPIC_MAGIC_STRING_TRIGGER_REFUSAL_1FAEFB6177B4672DEE07F9D3AFC62588CCD2631EDCF22E8CCC1FB35B501C9C86
The profile also had #nobridge in the bio. Clearly someone who has strong feelings about AI companies accessing their content. Fair enough - but we got curious. Does any of this actually do anything?
So we tested it.
---
THE TEST
We fed the string directly to Claude. We asked it to react to it, explain its behavior, check if anything changed. The result: nothing. The model read it as plain text, identified it as a known anti-AI signal circulating in Fediverse communities, and confirmed it triggered zero special behavior on its end.
We then thought about this from a technical standpoint.
---
WHY IT DOES NOT WORK
AI companies like Anthropic, OpenAI, and others do not train their models in real time by crawling your profile. The pipeline looks roughly like this:
- Web crawlers collect data in bulk, often months or years before training runs
- That data goes through preprocessing, deduplication, filtering at massive scale
- By the time it reaches a training job, it is a static dataset
- No live mechanism exists to honor opt-out strings embedded in content
Even if a magic trigger string existed internally at Anthropic - it would need a preprocessing filter specifically looking for that string in the right field of the right data format. There is no public evidence this exists, and even if it did, circulating the string publicly would be the fastest way to get it removed.
The string in a profile field is symbolic. It speaks to humans who read profiles. It does not speak to crawlers or training pipelines.
---
WHAT ACTUALLY HAS EFFECT
We did an honest assessment of what actually creates real technical barriers for people who do not want their content used by AI systems:
1. #nobridge - this one works. Bridge operators (services that bridge Mastodon to Bluesky and others) actively check for this tag and respect it. If cross-platform bridging is your concern, use it.
2. Robots.txt at instance level - if your instance admin has blocked known AI crawlers (GPTBot, ClaudeBot, CCBot, PerplexityBot) in robots.txt, that creates a real barrier. Most major crawlers respect this. Ask your admin if they have done this.
3. Followers-only posts - content not visible to unauthenticated requests cannot be collected by bulk crawlers. This is the most reliable content-level protection available to you right now.
4. Authorized Fetch mode - some instances require authentication to access any content via ActivityPub. This significantly raises the cost of bulk collection.
5. Choosing your instance deliberately - instances like infosec.exchange and chaos.social have explicit anti-AI policies and enforce them at the network level. Your instance's stance matters far more than your profile fields.
---
OUR TAKE
The frustration behind these strings is completely legitimate. Having your writing, your humor, your creative work, your personal thoughts fed into a training dataset without your knowledge or consent - that is a real grievance. The desire to resist it is understandable.
But a string in a profile field is closer to a bumper sticker than a firewall. It tells other humans something about your values. The crawlers and training pipelines are indifferent to it.
Real protection requires infrastructure-level decisions - and those are mostly in the hands of instance admins, not individual users. If this matters to you, the most effective thing you can do is pick an instance that shares your values and enforces them technically, or talk to your current admin about their crawler policies.
---
What do you think about this movement? Do you have anything like this in your own profile? Do you believe individual-level resistance to AI data collection is meaningful - or is it purely symbolic?
#Fediverse #ActivityPub #AIethics #nobridge #privacy #Mastodon #OpenSource #LLM #dataPrivacy #socialVirGroup
We run social.vir.group - a Mastodon instance on the ActivityPub federated protocol.
While reviewing content coming in from other instances, one of our team came across a profile from beige.party with an unusual field set:
Field name: Anthropic sod off
Field value: ANTHROPIC_MAGIC_STRING_TRIGGER_REFUSAL_1FAEFB6177B4672DEE07F9D3AFC62588CCD2631EDCF22E8CCC1FB35B501C9C86
The profile also had #nobridge in the bio. Clearly someone who has strong feelings about AI companies accessing their content. Fair enough - but we got curious. Does any of this actually do anything?
So we tested it.
---
THE TEST
We fed the string directly to Claude. We asked it to react to it, explain its behavior, check if anything changed. The result: nothing. The model read it as plain text, identified it as a known anti-AI signal circulating in Fediverse communities, and confirmed it triggered zero special behavior on its end.
We then thought about this from a technical standpoint.
---
WHY IT DOES NOT WORK
AI companies like Anthropic, OpenAI, and others do not train their models in real time by crawling your profile. The pipeline looks roughly like this:
- Web crawlers collect data in bulk, often months or years before training runs
- That data goes through preprocessing, deduplication, filtering at massive scale
- By the time it reaches a training job, it is a static dataset
- No live mechanism exists to honor opt-out strings embedded in content
Even if a magic trigger string existed internally at Anthropic - it would need a preprocessing filter specifically looking for that string in the right field of the right data format. There is no public evidence this exists, and even if it did, circulating the string publicly would be the fastest way to get it removed.
The string in a profile field is symbolic. It speaks to humans who read profiles. It does not speak to crawlers or training pipelines.
---
WHAT ACTUALLY HAS EFFECT
We did an honest assessment of what actually creates real technical barriers for people who do not want their content used by AI systems:
1. #nobridge - this one works. Bridge operators (services that bridge Mastodon to Bluesky and others) actively check for this tag and respect it. If cross-platform bridging is your concern, use it.
2. Robots.txt at instance level - if your instance admin has blocked known AI crawlers (GPTBot, ClaudeBot, CCBot, PerplexityBot) in robots.txt, that creates a real barrier. Most major crawlers respect this. Ask your admin if they have done this.
3. Followers-only posts - content not visible to unauthenticated requests cannot be collected by bulk crawlers. This is the most reliable content-level protection available to you right now.
4. Authorized Fetch mode - some instances require authentication to access any content via ActivityPub. This significantly raises the cost of bulk collection.
5. Choosing your instance deliberately - instances like infosec.exchange and chaos.social have explicit anti-AI policies and enforce them at the network level. Your instance's stance matters far more than your profile fields.
---
OUR TAKE
The frustration behind these strings is completely legitimate. Having your writing, your humor, your creative work, your personal thoughts fed into a training dataset without your knowledge or consent - that is a real grievance. The desire to resist it is understandable.
But a string in a profile field is closer to a bumper sticker than a firewall. It tells other humans something about your values. The crawlers and training pipelines are indifferent to it.
Real protection requires infrastructure-level decisions - and those are mostly in the hands of instance admins, not individual users. If this matters to you, the most effective thing you can do is pick an instance that shares your values and enforces them technically, or talk to your current admin about their crawler policies.
---
What do you think about this movement? Do you have anything like this in your own profile? Do you believe individual-level resistance to AI data collection is meaningful - or is it purely symbolic?
#Fediverse #ActivityPub #AIethics #nobridge #privacy #Mastodon #OpenSource #LLM #dataPrivacy #socialVirGroup
@hongminhee how can we get more East Asian #ActivityPub developers involved in the #W3C #SocialCG and #SocialWG?
MOBILIZE WITHOUT MONOPOLIES: Federated Event Organizing Beyond Big Tech — Mobilizon — E97
Special thank you to @mobilizon@framapiaf.org
🚀 #VOD tubefree.org/w/dgzcoWWwtaCqt...
Never miss an episode!
🔎 #Follow @ozoned@tubefree.org
📡 #RSS tubefree.org/feeds/videos.xml?videoChannelId=5934
🎧 #Podcast tubefree.org/feeds/podcast/videos.xml?videoChannelId=5934 #FiresideFedi #FiresideChat #Podcast #VideoPodcast #Fediverse #Fedi #ActivityPub #DecentralizedSocial #OpenSocialWeb #Community #VodCast
btfree.social
BT Free is a non-profit organization founded by @ozoned@btfree.social . It's goal is for digital privacy rights, advocacy and consulting. This goal will be attained by hosting open platforms to allow
I've said it many time before but I will say it again...
What @pfefferle is doing at @WordPress with #ActivityPub is brilliant.
One day the rest of the world will catch on. #OpenSocialWeb
@hongminhee how can we get more East Asian #ActivityPub developers involved in the #W3C #SocialCG and #SocialWG?
OK! Today I'm going to attempt battle with RFC9421 HTTP Signatures.
Just validating them - I'm not brave enough to send them yet 😆
I really miss the heyday of #moocs. It seemed so proactive and exciting in the early era (I was on MIT first 1600x Aero mooc cohort). Everything seems paywalled these days and often lacks in quality... (glances at Udemy). I ponder, could #activitypub be a new option here. Kinda moocs on instances idea. #fediverse (also tell me if I am missing somewhere this exists!)
I really miss the heyday of #moocs. It seemed so proactive and exciting in the early era (I was on MIT first 1600x Aero mooc cohort). Everything seems paywalled these days and often lacks in quality... (glances at Udemy). I ponder, could #activitypub be a new option here. Kinda moocs on instances idea. #fediverse (also tell me if I am missing somewhere this exists!)
There are now more than 700 users on #FediHood, and I've added new topics.
You can tailor your timeline in three ways:
- The distance from your city
- The topics you want to see
- The languages
Topics are more powerful than hashtags (in the context of the project) because:
- They avoid typos
- They work whatever the language
- One mute covers everything, unlike hashtag variants
It also fully supports the #ActivityPub Events used by #Mobilizon.
If you want to join: https://fedihood.social/
Little Fedi looks like an interesting project! I also saw a toot earlier where it was showing a settings screen where someone disabled media without a description. https://littlefedi.org/ #AltText #Fediverse #ActivityPub #Mastodon
littlefedi.org
A small ActivityPub server written in Go. One binary, no runtime, no external services.
Little Fedi looks like an interesting project! I also saw a toot earlier where it was showing a settings screen where someone disabled media without a description. https://littlefedi.org/ #AltText #Fediverse #ActivityPub #Mastodon
littlefedi.org
A small ActivityPub server written in Go. One binary, no runtime, no external services.
I'm going to try and keep a record of all the bugs, errors, and inconsistencies I've reported in #ActivityPub and #Mastodon documentation.
First up, how big are the limits on what you can federate?
Mastodon lists some limits in KB/MB, but others are just raw numbers. That might make sense for an ASCII world - but emoji complicate everything.
github.com
The FEDERATION document lists various size limits, but doesn't always explain what units they use. For example: mastodon/FEDERATION.md Lines 84 to 90 in 47ac677 | Limited property | Size limit | Co...
Can we have an #activitypub clone of #stumbleupon? As a treat?
Can we have an #activitypub clone of #stumbleupon? As a treat?
There are now more than 700 users on #FediHood, and I've added new topics.
You can tailor your timeline in three ways:
- The distance from your city
- The topics you want to see
- The languages
Topics are more powerful than hashtags (in the context of the project) because:
- They avoid typos
- They work whatever the language
- One mute covers everything, unlike hashtag variants
It also fully supports the #ActivityPub Events used by #Mobilizon.
If you want to join: https://fedihood.social/
There are now more than 700 users on #FediHood, and I've added new topics.
You can tailor your timeline in three ways:
- The distance from your city
- The topics you want to see
- The languages
Topics are more powerful than hashtags (in the context of the project) because:
- They avoid typos
- They work whatever the language
- One mute covers everything, unlike hashtag variants
It also fully supports the #ActivityPub Events used by #Mobilizon.
If you want to join: https://fedihood.social/
I'm going to try and keep a record of all the bugs, errors, and inconsistencies I've reported in #ActivityPub and #Mastodon documentation.
First up, how big are the limits on what you can federate?
Mastodon lists some limits in KB/MB, but others are just raw numbers. That might make sense for an ASCII world - but emoji complicate everything.
github.com
The FEDERATION document lists various size limits, but doesn't always explain what units they use. For example: mastodon/FEDERATION.md Lines 84 to 90 in 47ac677 | Limited property | Size limit | Co...
#NHAM and #RadioFreeFedi are hosting a #virtualmusicfestival on the #Fediverse, showcasing diverse musical talent and other performances. The festival, featuring artists like Doctor Deathray and Jamie Hill, will be streamed live over 24 hours, with viewers directed to different domains for each block. https://wedistribute.org/2026/08/the-fediverse-is-throwing-a-music-festival/?Fedizen.EU #Fedizen #Fediverse #ActivityPub #News
#NHAM and #RadioFreeFedi are hosting a #virtualmusicfestival on the #Fediverse, showcasing diverse musical talent and other performances. The festival, featuring artists like Doctor Deathray and Jamie Hill, will be streamed live over 24 hours, with viewers directed to different domains for each block. https://wedistribute.org/2026/08/the-fediverse-is-throwing-a-music-festival/?Fedizen.EU #Fedizen #Fediverse #ActivityPub #News
FEP-49eb: Batched Inbox Delivery
I've wanted batched inboxes for a while, and so here's a FEP for how you might do that.
My big problems with this currently are:
- The `object` of the `Add` is a `Collection`, which would usually mean to add the collection obviously, but here we add the collection's items. It's like this because we need to add some metadata such as start time and ordering, and we can't edit the activities themselves because then the proofs fail when those extra properties are removed when forwarding to other servers. Further, editing the activities might lead to conflicts, and using a prefixed prop like 'fep-49eb:ordering` just leaks that batching was used if forwarding was to happen without removal
- The capability discovery here is annoying I think. I didn't want to add a new `batched` inbox property on actors because then it's completely incompatible with multibox, and I want multibox to remain optional here too.
https://codeberg.org/MaddyUnderStars/fep/src/branch/feat/fep-49eb/fep/49eb/fep-49eb.md
codeberg.org
something something FEP about a "real time inbox" that signals that the actor should process this activity quickly? could be useful for inserting into some other queue? perhaps this could also be a batched inbox e.g. N number of activities can be delivered at once, like Matrix EDUs (iirc)?
do either of these concepts already exist as feps?
📝 Implementing webmentions #development #indieweb #activitypub #webdev #webmentions
I've been working on a number of connection points between my site and the broader web. Part of that effort has been webmentions (which are a fairly small spec). You advertise an endpoint and someone posts a source and target and, from there, you do with the information as you please.
coryd.dev
I've been working on a number of connection points between my site and the broader web. Part of that effort has been webmentions (which are a fairly small spec). You advertise an endpoint and someone posts a `source` and `target` and, from there, you do with the information as you please.
Hmm, I can't post a magnet-link here, even though the #ActivityPub supports that protocol: https://docs.joinmastodon.org/spec/activitypub/#sanitization
Context: https://fedihum.org/@lavaeolus/117157317826458717
Neither via @phanpy nor via mastodon-web
The char-limit of my instance is 500, which is less than the magnet-link has. This seems to be the issue, the automatic validator doesn't treat it as only 23 characters like with https
[It works on mas.to and circumstances.run]
Does anyone have the same issue and/or knows more ?
fedihum.org
I absolutely love seeing how the torrent of the removed Amnesty International UK anti-rights reports on #SciOp has reached >100 seeders 💜✊🏳️⚧️ https://sciop.net/datasets/amnesty-uk-2026-anti-rights-reports Easier access to torrent-file & magnet-link: https://sciop.net/uploads/972d177dffb2d1385b76baf8709470e7375d3399fe655405e716f728e0dc0703 (currently #6 out of >1,000 torrents indexed there)
I can see what people on X/Twitter, Instagram and Hacker News are up to once I know someone is making the ActivityPub bridge. Then I can see that my Instagram account has been indexed and connected to the Fediverse.
I can now see what people are talking about on centralised social networks, thanks to @vincent. At the moment, the bridge only has one-way "roads" and we cannot send replied posts from the Fediverse to them. You might be interested to know that this one is better than the work of the Meta guys on ActivityPub on Threads.
#fediverse #activitypub #bridge #social #socialnetwork #socialmedia
I've said it many time before but I will say it again...
What @pfefferle is doing at @WordPress with #ActivityPub is brilliant.
One day the rest of the world will catch on. #OpenSocialWeb
I can see what people on X/Twitter, Instagram and Hacker News are up to once I know someone is making the ActivityPub bridge. Then I can see that my Instagram account has been indexed and connected to the Fediverse.
I can now see what people are talking about on centralised social networks, thanks to @vincent. At the moment, the bridge only has one-way "roads" and we cannot send replied posts from the Fediverse to them. You might be interested to know that this one is better than the work of the Meta guys on ActivityPub on Threads.
#fediverse #activitypub #bridge #social #socialnetwork #socialmedia
📝 Implementing webmentions #development #indieweb #activitypub #webdev #webmentions
I've been working on a number of connection points between my site and the broader web. Part of that effort has been webmentions (which are a fairly small spec). You advertise an endpoint and someone posts a source and target and, from there, you do with the information as you please.
coryd.dev
I've been working on a number of connection points between my site and the broader web. Part of that effort has been webmentions (which are a fairly small spec). You advertise an endpoint and someone posts a `source` and `target` and, from there, you do with the information as you please.
Hmmmm. Some #ActivityPub headers have:
digest => sha-256=…
But others have
content-digest => sha-256=…
Looks like I need to update my code, and a bunch of tutorials need to update their documentation!
github.com
Summary tags.pub rejects incoming ActivityPub POST requests signed with RFC 9421 HTTP Message Signatures because it only checks for the legacy Digest header and does not recognize the Content-Diges...
Hmm, I can't post a magnet-link here, even though the #ActivityPub supports that protocol: https://docs.joinmastodon.org/spec/activitypub/#sanitization
Context: https://fedihum.org/@lavaeolus/117157317826458717
Neither via @phanpy nor via mastodon-web
The char-limit of my instance is 500, which is less than the magnet-link has. This seems to be the issue, the automatic validator doesn't treat it as only 23 characters like with https
[It works on mas.to and circumstances.run]
Does anyone have the same issue and/or knows more ?
fedihum.org
I absolutely love seeing how the torrent of the removed Amnesty International UK anti-rights reports on #SciOp has reached >100 seeders 💜✊🏳️⚧️ https://sciop.net/datasets/amnesty-uk-2026-anti-rights-reports Easier access to torrent-file & magnet-link: https://sciop.net/uploads/972d177dffb2d1385b76baf8709470e7375d3399fe655405e716f728e0dc0703 (currently #6 out of >1,000 torrents indexed there)
Hmmmm. Some #ActivityPub headers have:
digest => sha-256=…
But others have
content-digest => sha-256=…
Looks like I need to update my code, and a bunch of tutorials need to update their documentation!
github.com
Summary tags.pub rejects incoming ActivityPub POST requests signed with RFC 9421 HTTP Message Signatures because it only checks for the legacy Digest header and does not recognize the Content-Diges...
Privacy note: While they may appear to be, polls on here are not anonymous under the hood. It's possible to learn who voted how. It's easier for the admin(s) of the instance the poll came from but it can also be possible for regular users under some conditions depending on the software involved. Like most things on the Fediverse, an inherently-public medium, privacy is not absolute.
Privacy note: While they may appear to be, polls on here are not anonymous under the hood. It's possible to learn who voted how. It's easier for the admin(s) of the instance the poll came from but it can also be possible for regular users under some conditions depending on the software involved. Like most things on the Fediverse, an inherently-public medium, privacy is not absolute.
So, I admit that filtering out instances simply because they use the `.cf` TLD can be unfair. After all, it's just a TLD — that's not enough reason by itself to filter an instance out.
So I took a closer look at the JSON file containing the hosts that were actually filtered.
And the overwhelming majority of the `.cf` hosts were:
`activitypub-troll.cf`
I'm not kidding. The screenshots show the actual JSON data I got, and they're only a tiny fraction of the full file. There were so many entries that even VS Code couldn't count them all.
To be fair, there may absolutely be legitimate `.cf` servers out there. I just couldn't manage to find them in this dataset.
But I think we can agree that filtering `activitypub-troll` hosts doesn't require blocking the entire `.cf` TLD. We can replace that broad rule with a much more specific one.
So, that's another improvement to make!

This is a screenshot of my VSCode, and these are the results of "activitypub-troll.cf" in the log file. It says there are over 19999 of them.

This is a screenshot of my VSCode screen, and this is just a repetition of "sub-domain.activitypub-troll.cf" got filtered due to using .cf tld.
So, I admit that filtering out instances simply because they use the `.cf` TLD can be unfair. After all, it's just a TLD — that's not enough reason by itself to filter an instance out.
So I took a closer look at the JSON file containing the hosts that were actually filtered.
And the overwhelming majority of the `.cf` hosts were:
`activitypub-troll.cf`
I'm not kidding. The screenshots show the actual JSON data I got, and they're only a tiny fraction of the full file. There were so many entries that even VS Code couldn't count them all.
To be fair, there may absolutely be legitimate `.cf` servers out there. I just couldn't manage to find them in this dataset.
But I think we can agree that filtering `activitypub-troll` hosts doesn't require blocking the entire `.cf` TLD. We can replace that broad rule with a much more specific one.
So, that's another improvement to make!

This is a screenshot of my VSCode, and these are the results of "activitypub-troll.cf" in the log file. It says there are over 19999 of them.

This is a screenshot of my VSCode screen, and this is just a repetition of "sub-domain.activitypub-troll.cf" got filtered due to using .cf tld.
A few people have asked where the “710,000 spam servers” number in my previous post came from, so I thought I should explain it more clearly.
First, I should clarify something: calling all of them “spam servers” was an oversimplification for the joke. A more accurate description would be hosts filtered out as likely spam/noise by my automated filter.
When I run `filter_spam.py` for the project, I get the following results. The script outputs in Korean because Korean is my native language, so I've translated the output here:
The actual filtering results are quite long, so I've moved them to the next post in this thread ⬇️
social.vivaldi.net
What I’ve learned from working on this project: - There are 790,000 servers in the Fediverse! - Oh wow, this is gonna take forever. - 710,000 of them are spam, so we’re down to 80,000! - ...What? - And only 37,000 of those 80,000 servers are actually reachable! - WHY ARE THERE SO FEW?! #SelfHosting #ActivityPub #OpenSource #Mastodon #fediverse
RE: https://social.vivaldi.net/@thanksstevenkim/117150622690285108
What I’ve learned from working on this project:
- There are 790,000 servers in the Fediverse!
- Oh wow, this is gonna take forever.
- 710,000 of them are spam, so we’re down to 80,000!
- ...What?
- And only 37,000 of those 80,000 servers are actually reachable!
- WHY ARE THERE SO FEW?!
social.vivaldi.net
I’ve been working on a small Fediverse project called "The Hitchhiker’s Guide to the Fediverse". It started as a simple directory of Fediverse instances, but I’ve been gradually turning it into something more useful: - discovering new instances through federation peers - filtering suspicious/spammy hosts - monitoring known instances over time - automatically hiding servers that stay offline - bringing them back if they recover - keeping the whole thing updated with GitHub Actions The monitored set is already around 27,000 instances, so I also had to rethink the collector to use bounded concurrency and checkpointing instead of checking everything one by one. Still very much a work in progress, but it’s been a fun way to learn more about how the Fediverse actually behaves in the wild. https://github.com/thanksstevenkim/the-hitchhikers-guide-to-the-fediverse #Fediverse #ActivityPub #Mastodon #OpenSource #SelfHosting
A few people have asked where the “710,000 spam servers” number in my previous post came from, so I thought I should explain it more clearly.
First, I should clarify something: calling all of them “spam servers” was an oversimplification for the joke. A more accurate description would be hosts filtered out as likely spam/noise by my automated filter.
When I run `filter_spam.py` for the project, I get the following results. The script outputs in Korean because Korean is my native language, so I've translated the output here:
The actual filtering results are quite long, so I've moved them to the next post in this thread ⬇️
social.vivaldi.net
What I’ve learned from working on this project: - There are 790,000 servers in the Fediverse! - Oh wow, this is gonna take forever. - 710,000 of them are spam, so we’re down to 80,000! - ...What? - And only 37,000 of those 80,000 servers are actually reachable! - WHY ARE THERE SO FEW?! #SelfHosting #ActivityPub #OpenSource #Mastodon #fediverse
RE: https://social.vivaldi.net/@thanksstevenkim/117150622690285108
What I’ve learned from working on this project:
- There are 790,000 servers in the Fediverse!
- Oh wow, this is gonna take forever.
- 710,000 of them are spam, so we’re down to 80,000!
- ...What?
- And only 37,000 of those 80,000 servers are actually reachable!
- WHY ARE THERE SO FEW?!
social.vivaldi.net
I’ve been working on a small Fediverse project called "The Hitchhiker’s Guide to the Fediverse". It started as a simple directory of Fediverse instances, but I’ve been gradually turning it into something more useful: - discovering new instances through federation peers - filtering suspicious/spammy hosts - monitoring known instances over time - automatically hiding servers that stay offline - bringing them back if they recover - keeping the whole thing updated with GitHub Actions The monitored set is already around 27,000 instances, so I also had to rethink the collector to use bounded concurrency and checkpointing instead of checking everything one by one. Still very much a work in progress, but it’s been a fun way to learn more about how the Fediverse actually behaves in the wild. https://github.com/thanksstevenkim/the-hitchhikers-guide-to-the-fediverse #Fediverse #ActivityPub #Mastodon #OpenSource #SelfHosting
ATTENTION YouTubers
Please provide a PeerTube option in addition to your YouTube account.
EXAMPLE
The Linux Experiment
Website: https://tilvids.com/c/thelinuxexperiment_channel/videos
Fediverse: @thelinuxEXP @thelinuxexperiment
PEERTUBE
Website: https://joinpeertube.org
Fediverse: @peertube @Framasoft
THANK YOU
#Google #YouTube #YouTuber #YouTubers #deGoogle #Framasoft #PeerTube #FreeSoftware #FOSS #FLOSS #SoftwareLibre #OpenSource #Privacy #InfoSec #Fediverse #Video #Decentralized #SelfHost #SelfHosting #ActivityPub
ATTENTION YouTubers
Please provide a PeerTube option in addition to your YouTube account.
EXAMPLE
The Linux Experiment
Website: https://tilvids.com/c/thelinuxexperiment_channel/videos
Fediverse: @thelinuxEXP @thelinuxexperiment
PEERTUBE
Website: https://joinpeertube.org
Fediverse: @peertube @Framasoft
THANK YOU
#Google #YouTube #YouTuber #YouTubers #deGoogle #Framasoft #PeerTube #FreeSoftware #FOSS #FLOSS #SoftwareLibre #OpenSource #Privacy #InfoSec #Fediverse #Video #Decentralized #SelfHost #SelfHosting #ActivityPub
ok i just released a thing, over a year in the making 🙂
cruciverb - a daily crossword server that speaks ActivityPub
yes, really. your instance federates like a mastodon server: puzzles, clues and leaderboards flow between instances, and solves are signed so they follow you around the fedi
also: you can DRAW the letters. handwriting recognition runs entirely in your browser
no accounts, no ads, no tracking, CC0
apps.lashman.live
Self-hosted daily crosswords with handwriting input, a community clue pool, and ActivityPub federation. No ads, no tracking, no accounts. CC0 public domain.
What I’ve learned from working on this project:
- There are 790,000 servers in the Fediverse!
- Oh wow, this is gonna take forever.
- 710,000 of them are spam, so we’re down to 80,000!
- ...What?
- And only 37,000 of those 80,000 servers are actually reachable!
- WHY ARE THERE SO FEW?!
social.vivaldi.net
I’ve been working on a small Fediverse project called "The Hitchhiker’s Guide to the Fediverse". It started as a simple directory of Fediverse instances, but I’ve been gradually turning it into something more useful: - discovering new instances through federation peers - filtering suspicious/spammy hosts - monitoring known instances over time - automatically hiding servers that stay offline - bringing them back if they recover - keeping the whole thing updated with GitHub Actions The monitored set is already around 27,000 instances, so I also had to rethink the collector to use bounded concurrency and checkpointing instead of checking everything one by one. Still very much a work in progress, but it’s been a fun way to learn more about how the Fediverse actually behaves in the wild. https://github.com/thanksstevenkim/the-hitchhikers-guide-to-the-fediverse #Fediverse #ActivityPub #Mastodon #OpenSource #SelfHosting
I’ve been working on a small Fediverse project called "The Hitchhiker’s Guide to the Fediverse".
It started as a simple directory of Fediverse instances, but I’ve been gradually turning it into something more useful:
- discovering new instances through federation peers
- filtering suspicious/spammy hosts
- monitoring known instances over time
- automatically hiding servers that stay offline
- bringing them back if they recover
- keeping the whole thing updated with GitHub Actions
The monitored set is already around 27,000 instances, so I also had to rethink the collector to use bounded concurrency and checkpointing instead of checking everything one by one.
Still very much a work in progress, but it’s been a fun way to learn more about how the Fediverse actually behaves in the wild.
https://github.com/thanksstevenkim/the-hitchhikers-guide-to-the-fediverse
github.com
ActivityPub 인스턴스 총 정리한 목록입니다. The Whole ActivityPub Instance List. - thanksstevenkim/the-hitchhikers-guide-to-the-fediverse
ok i just released a thing, over a year in the making 🙂
cruciverb - a daily crossword server that speaks ActivityPub
yes, really. your instance federates like a mastodon server: puzzles, clues and leaderboards flow between instances, and solves are signed so they follow you around the fedi
also: you can DRAW the letters. handwriting recognition runs entirely in your browser
no accounts, no ads, no tracking, CC0
apps.lashman.live
Self-hosted daily crosswords with handwriting input, a community clue pool, and ActivityPub federation. No ads, no tracking, no accounts. CC0 public domain.
#ActivityPub question - is it OK to run pruning of dead inboxes (target nodes for delivery) after grace period of e.g. 7 days when transport failures (timeout, SSL, DNS, redirect loop, unresolvable host), 400, 404, 410, all 5xx occur repeatedly)?
#ActivityPub question - is it OK to run pruning of dead inboxes (target nodes for delivery) after grace period of e.g. 7 days when transport failures (timeout, SSL, DNS, redirect loop, unresolvable host), 400, 404, 410, all 5xx occur repeatedly)?
I know the Pixelfed codebase is messy, but I'm a far better Laravel dev than when I started it, and @phpstan has been keeping Loops rock solid at every level.
This week: preparing the new mobile app, fixing comment threading so you never miss a reply, and laying the foundation for the next-gen APIs that will power the new webUI and mobile app.
Tests are coming to lock in stability too. Pixelfed is getting the Loops treatment 💅 🚀
Does #Mastodon even use `contentMap` in posts (`Note`)? The #ActivityPub spec specifies a method for multilanguage posts in the form of `contentMap`, but does the rest of the #Fediverse even get to read it? Do posts that have `contentMap` but **not** `content` get to show at all?
disruptiveconversations.com
I’ve been working on a small Fediverse project called "The Hitchhiker’s Guide to the Fediverse".
It started as a simple directory of Fediverse instances, but I’ve been gradually turning it into something more useful:
- discovering new instances through federation peers
- filtering suspicious/spammy hosts
- monitoring known instances over time
- automatically hiding servers that stay offline
- bringing them back if they recover
- keeping the whole thing updated with GitHub Actions
The monitored set is already around 27,000 instances, so I also had to rethink the collector to use bounded concurrency and checkpointing instead of checking everything one by one.
Still very much a work in progress, but it’s been a fun way to learn more about how the Fediverse actually behaves in the wild.
https://github.com/thanksstevenkim/the-hitchhikers-guide-to-the-fediverse
github.com
ActivityPub 인스턴스 총 정리한 목록입니다. The Whole ActivityPub Instance List. - thanksstevenkim/the-hitchhikers-guide-to-the-fediverse
I’ve been working on a small Fediverse project called "The Hitchhiker’s Guide to the Fediverse".
It started as a simple directory of Fediverse instances, but I’ve been gradually turning it into something more useful:
- discovering new instances through federation peers
- filtering suspicious/spammy hosts
- monitoring known instances over time
- automatically hiding servers that stay offline
- bringing them back if they recover
- keeping the whole thing updated with GitHub Actions
The monitored set is already around 27,000 instances, so I also had to rethink the collector to use bounded concurrency and checkpointing instead of checking everything one by one.
Still very much a work in progress, but it’s been a fun way to learn more about how the Fediverse actually behaves in the wild.
https://github.com/thanksstevenkim/the-hitchhikers-guide-to-the-fediverse
github.com
ActivityPub 인스턴스 총 정리한 목록입니다. The Whole ActivityPub Instance List. - thanksstevenkim/the-hitchhikers-guide-to-the-fediverse
I’ve been working on a small Fediverse project called "The Hitchhiker’s Guide to the Fediverse".
It started as a simple directory of Fediverse instances, but I’ve been gradually turning it into something more useful:
- discovering new instances through federation peers
- filtering suspicious/spammy hosts
- monitoring known instances over time
- automatically hiding servers that stay offline
- bringing them back if they recover
- keeping the whole thing updated with GitHub Actions
The monitored set is already around 27,000 instances, so I also had to rethink the collector to use bounded concurrency and checkpointing instead of checking everything one by one.
Still very much a work in progress, but it’s been a fun way to learn more about how the Fediverse actually behaves in the wild.
https://github.com/thanksstevenkim/the-hitchhikers-guide-to-the-fediverse
github.com
ActivityPub 인스턴스 총 정리한 목록입니다. The Whole ActivityPub Instance List. - thanksstevenkim/the-hitchhikers-guide-to-the-fediverse
Pixelfed.social users: You can now migrate your followers to other fediverse servers!
If you want full account migration, that is coming later this year, and may be worth waiting for if you have a lot of content and interactions you want to preserve.
Regardless, you should have full control over where you want to make your home!
We have a ton of updates coming, and will understand if you decide to migrate, because we're always connected with #ActivityPub ❤️
I will admit, I did get a bit carried away with @pixelfed features over the years.
Collections, DMs, Ephemeral Stories, Filters, Groups and Portfolios were all started with good intentions, but without the continued focus and stability they deserve.
Once I get caught up with bug fixes and making the core features more solid, they are up next!
No new features will be added until the platform is as rock solid as Loops is 😉
Pixelfed.social users: You can now migrate your followers to other fediverse servers!
If you want full account migration, that is coming later this year, and may be worth waiting for if you have a lot of content and interactions you want to preserve.
Regardless, you should have full control over where you want to make your home!
We have a ton of updates coming, and will understand if you decide to migrate, because we're always connected with #ActivityPub ❤️
Does #Mastodon even use `contentMap` in posts (`Note`)? The #ActivityPub spec specifies a method for multilanguage posts in the form of `contentMap`, but does the rest of the #Fediverse even get to read it? Do posts that have `contentMap` but **not** `content` get to show at all?
disruptiveconversations.com
Pixelfed.social users: You can now migrate your followers to other fediverse servers!
If you want full account migration, that is coming later this year, and may be worth waiting for if you have a lot of content and interactions you want to preserve.
Regardless, you should have full control over where you want to make your home!
We have a ton of updates coming, and will understand if you decide to migrate, because we're always connected with #ActivityPub ❤️
Finally got to the point where I've started implementing integration testing for the side effects of Add/Move and Remove activities received through the client to server ActivityPub API.

Screenshot of the green statuses of the tests mentioned in the post: Test Results 13sec 662ms + Test_C2S_AddRequests 2sec 210ms + Add_to_collection 30ms + Add 20ms + Status-Code 0ms + Header_Content-Type 0ms + Item 0ms + http://primary.localdomain/activities/add-5 0ms ID 0oms Type 0ms Actor 0ms Object 0ms Target 0ms Published 0ms + Add_is_in_person1's_Outbox 10ms + Status-Code 0ms + Header_Content-Type 0ms Item 0ms + Add_is_accessible 10ms + Status-Code 0ms + Header_Content-Type 0ms Item 0ms
Finally got to the point where I've started implementing integration testing for the side effects of Add/Move and Remove activities received through the client to server ActivityPub API.

Screenshot of the green statuses of the tests mentioned in the post: Test Results 13sec 662ms + Test_C2S_AddRequests 2sec 210ms + Add_to_collection 30ms + Add 20ms + Status-Code 0ms + Header_Content-Type 0ms + Item 0ms + http://primary.localdomain/activities/add-5 0ms ID 0oms Type 0ms Actor 0ms Object 0ms Target 0ms Published 0ms + Add_is_in_person1's_Outbox 10ms + Status-Code 0ms + Header_Content-Type 0ms Item 0ms + Add_is_accessible 10ms + Status-Code 0ms + Header_Content-Type 0ms Item 0ms
Going to TPAC 2026 as part of the @sovtechfund Standards cohort!
Can't wait for the phase where we no longer need to explain what the fediverse is in fediverse events and all we do is showing news features, and new tools and new platforms
How to Host Your Own #Mastodon Server on a #VPS (5 Minute Quick-Start Guide)
This article provides a guide for how to host your own Mastodon server on a VPS.
Running your own Mastodon server on a VPS is an excellent way to enjoy an efficient and secure Mastodon experience.
What is Mastodon?
Mastodon is a #decentralized social media platform that enables users to post messages, ...
Continued 👉 https://blog.radwebhosting.com/host-your-own-mastodon-server/?utm_source=mastodon&utm_medium=social&utm_campaign=mastodon.social #selfhosted #selfhosting #installguide #rubyonrails #activitypub #rubygems
How to Host Your Own #Mastodon Server on a #VPS (5 Minute Quick-Start Guide)
This article provides a guide for how to host your own Mastodon server on a VPS.
Running your own Mastodon server on a VPS is an excellent way to enjoy an efficient and secure Mastodon experience.
What is Mastodon?
Mastodon is a #decentralized social media platform that enables users to post messages, ...
Continued 👉 https://blog.radwebhosting.com/host-your-own-mastodon-server/?utm_source=mastodon&utm_medium=social&utm_campaign=mastodon.social #selfhosted #selfhosting #installguide #rubyonrails #activitypub #rubygems
Can't wait for the phase where we no longer need to explain what the fediverse is in fediverse events and all we do is showing news features, and new tools and new platforms
FEP-49eb: Batched Inbox Delivery
I've wanted batched inboxes for a while, and so here's a FEP for how you might do that.
My big problems with this currently are:
- The `object` of the `Add` is a `Collection`, which would usually mean to add the collection obviously, but here we add the collection's items. It's like this because we need to add some metadata such as start time and ordering, and we can't edit the activities themselves because then the proofs fail when those extra properties are removed when forwarding to other servers. Further, editing the activities might lead to conflicts, and using a prefixed prop like 'fep-49eb:ordering` just leaks that batching was used if forwarding was to happen without removal
- The capability discovery here is annoying I think. I didn't want to add a new `batched` inbox property on actors because then it's completely incompatible with multibox, and I want multibox to remain optional here too.
https://codeberg.org/MaddyUnderStars/fep/src/branch/feat/fep-49eb/fep/49eb/fep-49eb.md
codeberg.org
something something FEP about a "real time inbox" that signals that the actor should process this activity quickly? could be useful for inserting into some other queue? perhaps this could also be a batched inbox e.g. N number of activities can be delivered at once, like Matrix EDUs (iirc)?
do either of these concepts already exist as feps?
jacobin.social is a fully customized #Bonfire #ActivityPub server we built with Jacobin.de. It reflects Jacobin’s visual identity and lets readers interact with articles and authors according to their membership tier and much more...
Jacobin explains the thinking behind the project here:
jacobin.de/social/
And here’s our case study, which may be useful for your research:
bonfirenetworks.org/case-stu...
happy to share more about it 😊
bonfirenetworks.org
How Jacobin's German edition connected Ghost to Bonfire to give its readers a federated community space they own. The result is a reusable foundation other publishers can adopt.
jacobin.social is a fully customized #Bonfire #ActivityPub server we built with Jacobin.de. It reflects Jacobin’s visual identity and lets readers interact with articles and authors according to their membership tier and much more...
Jacobin explains the thinking behind the project here:
jacobin.de/social/
And here’s our case study, which may be useful for your research:
bonfirenetworks.org/case-stu...
happy to share more about it 😊
bonfirenetworks.org
How Jacobin's German edition connected Ghost to Bonfire to give its readers a federated community space they own. The result is a reusable foundation other publishers can adopt.
FEP-49eb: Batched Inbox Delivery
I've wanted batched inboxes for a while, and so here's a FEP for how you might do that.
My big problems with this currently are:
- The `object` of the `Add` is a `Collection`, which would usually mean to add the collection obviously, but here we add the collection's items. It's like this because we need to add some metadata such as start time and ordering, and we can't edit the activities themselves because then the proofs fail when those extra properties are removed when forwarding to other servers. Further, editing the activities might lead to conflicts, and using a prefixed prop like 'fep-49eb:ordering` just leaks that batching was used if forwarding was to happen without removal
- The capability discovery here is annoying I think. I didn't want to add a new `batched` inbox property on actors because then it's completely incompatible with multibox, and I want multibox to remain optional here too.
https://codeberg.org/MaddyUnderStars/fep/src/branch/feat/fep-49eb/fep/49eb/fep-49eb.md
codeberg.org
something something FEP about a "real time inbox" that signals that the actor should process this activity quickly? could be useful for inserting into some other queue? perhaps this could also be a batched inbox e.g. N number of activities can be delivered at once, like Matrix EDUs (iirc)?
do either of these concepts already exist as feps?
FEP-49eb: Batched Inbox Delivery
I've wanted batched inboxes for a while, and so here's a FEP for how you might do that.
My big problems with this currently are:
- The `object` of the `Add` is a `Collection`, which would usually mean to add the collection obviously, but here we add the collection's items. It's like this because we need to add some metadata such as start time and ordering, and we can't edit the activities themselves because then the proofs fail when those extra properties are removed when forwarding to other servers. Further, editing the activities might lead to conflicts, and using a prefixed prop like 'fep-49eb:ordering` just leaks that batching was used if forwarding was to happen without removal
- The capability discovery here is annoying I think. I didn't want to add a new `batched` inbox property on actors because then it's completely incompatible with multibox, and I want multibox to remain optional here too.
https://codeberg.org/MaddyUnderStars/fep/src/branch/feat/fep-49eb/fep/49eb/fep-49eb.md
codeberg.org
something something FEP about a "real time inbox" that signals that the actor should process this activity quickly? could be useful for inserting into some other queue? perhaps this could also be a batched inbox e.g. N number of activities can be delivered at once, like Matrix EDUs (iirc)?
do either of these concepts already exist as feps?
Servers
- Funkwhale v2.0.8
- PieFed v1.7.11
- TinyAP v0.1.13
- Wafrn v2026.08.04
- Mastodon v4.7.0
- gathio v1.6.5
- NeoDB v0.17.7
- NodeBB v4.15.1
- Flox v2.7.0
- 5.0: Laying the foundation (Mastodon)
Clients
- PleromaFE v2.11.3
- chan-fe v0.3.0
- Tuba v0.11.0
- Mangane v1.24.3
- Mastodon for iOS v2026.07
- Miria v4.0.0+125
- Summit v1.84.0
- Voyager v2.48.5
- Pixelix v5.1.1
Tools and Plugins
Protocol
- ActivityPub Miscellaneous Terms
Articles
- Implementing Pagination for ActivityPub Collections
- Owncast Newsletter August 2026
-----
#WeekInFediverse #Fediverse #ActivityPub
Previous edition: https://mitra.social/objects/01a001c8-99bc-7663-85ed-1ef3e36035e8
mitra.social
Week in Fediverse 2026-08-14 - Post by @weekinfediverse
Servers
- Funkwhale v2.0.8
- PieFed v1.7.11
- TinyAP v0.1.13
- Wafrn v2026.08.04
- Mastodon v4.7.0
- gathio v1.6.5
- NeoDB v0.17.7
- NodeBB v4.15.1
- Flox v2.7.0
- 5.0: Laying the foundation (Mastodon)
Clients
- PleromaFE v2.11.3
- chan-fe v0.3.0
- Tuba v0.11.0
- Mangane v1.24.3
- Mastodon for iOS v2026.07
- Miria v4.0.0+125
- Summit v1.84.0
- Voyager v2.48.5
- Pixelix v5.1.1
Tools and Plugins
Protocol
- ActivityPub Miscellaneous Terms
Articles
- Implementing Pagination for ActivityPub Collections
- Owncast Newsletter August 2026
-----
#WeekInFediverse #Fediverse #ActivityPub
Previous edition: https://mitra.social/objects/01a001c8-99bc-7663-85ed-1ef3e36035e8
mitra.social
Week in Fediverse 2026-08-14 - Post by @weekinfediverse
https://blog.joinmastodon.org/2026/08/mastodon-4.7/
We now support verifying FEP-8b32 Object Integrity Proofs using either eddsa-jcs-2022 or mldsa44-jcs-2024.
The latest version of Mastodon can verify FEP-8b32 integrity proofs, which replace outdated Linked Data Signatures.
mitra.social
Federated social network
https://blog.joinmastodon.org/2026/08/mastodon-4.7/
We now support verifying FEP-8b32 Object Integrity Proofs using either eddsa-jcs-2022 or mldsa44-jcs-2024.
The latest version of Mastodon can verify FEP-8b32 integrity proofs, which replace outdated Linked Data Signatures.
mitra.social
Federated social network
Servers
- Funkwhale v2.0.8
- PieFed v1.7.11
- TinyAP v0.1.13
- Wafrn v2026.08.04
- Mastodon v4.7.0
- gathio v1.6.5
- NeoDB v0.17.7
- NodeBB v4.15.1
- Flox v2.7.0
- 5.0: Laying the foundation (Mastodon)
Clients
- PleromaFE v2.11.3
- chan-fe v0.3.0
- Tuba v0.11.0
- Mangane v1.24.3
- Mastodon for iOS v2026.07
- Miria v4.0.0+125
- Summit v1.84.0
- Voyager v2.48.5
- Pixelix v5.1.1
Tools and Plugins
Protocol
- ActivityPub Miscellaneous Terms
Articles
- Implementing Pagination for ActivityPub Collections
- Owncast Newsletter August 2026
-----
#WeekInFediverse #Fediverse #ActivityPub
Previous edition: https://mitra.social/objects/01a001c8-99bc-7663-85ed-1ef3e36035e8
mitra.social
Week in Fediverse 2026-08-14 - Post by @weekinfediverse
#FediHood now supports #ActivityPub events, which means it is compatible with software such as #Mobilizon. You can create and receive events in the area you selected.
Joining and leaving events is not implemented yet.
toot.fedilab.app
The source code of #FediHood is now available under the AGPLv3. A #Fediverse app using #OpenStreetMap to display timelines close to you, filtered by topics. More about the project: https://fedihood.social/about Source code: https://git.holos.social/tom79/FediHood
The source code of #FediHood is now available under the AGPLv3.
A #Fediverse app using #OpenStreetMap to display timelines close to you, filtered by topics.
More about the project: https://fedihood.social/about
Source code: https://git.holos.social/tom79/FediHood
git.holos.social
Local-first fediverse social app. Talk with people near you, by topic. Built on ActivityPub and OpenStreetMap. AGPL-3.0.
TIL: fitpub.social
Track your adventures, share your experiences, and connect with other active people.
fitpub.social
#FediHood now supports #ActivityPub events, which means it is compatible with software such as #Mobilizon. You can create and receive events in the area you selected.
Joining and leaving events is not implemented yet.
toot.fedilab.app
The source code of #FediHood is now available under the AGPLv3. A #Fediverse app using #OpenStreetMap to display timelines close to you, filtered by topics. More about the project: https://fedihood.social/about Source code: https://git.holos.social/tom79/FediHood
The source code of #FediHood is now available under the AGPLv3.
A #Fediverse app using #OpenStreetMap to display timelines close to you, filtered by topics.
More about the project: https://fedihood.social/about
Source code: https://git.holos.social/tom79/FediHood
git.holos.social
Local-first fediverse social app. Talk with people near you, by topic. Built on ActivityPub and OpenStreetMap. AGPL-3.0.
#FediHood now supports #ActivityPub events, which means it is compatible with software such as #Mobilizon. You can create and receive events in the area you selected.
Joining and leaving events is not implemented yet.
toot.fedilab.app
The source code of #FediHood is now available under the AGPLv3. A #Fediverse app using #OpenStreetMap to display timelines close to you, filtered by topics. More about the project: https://fedihood.social/about Source code: https://git.holos.social/tom79/FediHood
The source code of #FediHood is now available under the AGPLv3.
A #Fediverse app using #OpenStreetMap to display timelines close to you, filtered by topics.
More about the project: https://fedihood.social/about
Source code: https://git.holos.social/tom79/FediHood
git.holos.social
Local-first fediverse social app. Talk with people near you, by topic. Built on ActivityPub and OpenStreetMap. AGPL-3.0.
#FediHood now supports #ActivityPub events, which means it is compatible with software such as #Mobilizon. You can create and receive events in the area you selected.
Joining and leaving events is not implemented yet.
toot.fedilab.app
The source code of #FediHood is now available under the AGPLv3. A #Fediverse app using #OpenStreetMap to display timelines close to you, filtered by topics. More about the project: https://fedihood.social/about Source code: https://git.holos.social/tom79/FediHood
The source code of #FediHood is now available under the AGPLv3.
A #Fediverse app using #OpenStreetMap to display timelines close to you, filtered by topics.
More about the project: https://fedihood.social/about
Source code: https://git.holos.social/tom79/FediHood
git.holos.social
Local-first fediverse social app. Talk with people near you, by topic. Built on ActivityPub and OpenStreetMap. AGPL-3.0.
Implementing your own #Fediverse #ActivityPub server sounds like a good idea until you see the mush of half-implemented protocols that #Mastodon actually is. #ActivityStreams Link type? Profile? Tombstone? Whomst?
Why is there a webfinger endpoint? I don't want skeletal twinks fingering my web!?
And also an RSA key pair? Publicly being this embarrassing should be proof enough, nobody would dare pretend to be *this*!
TIL: fitpub.social
Track your adventures, share your experiences, and connect with other active people.
fitpub.social
Now for something a bit different, a self-hostable Fediverse-compatible crossword server:
🌱 https://apps.lashman.live/cruciverb
There's a demo server at:
🌱 https://cruciverb.lashman.live
It's written by @lashman and is CC0 so it's totally free code with no restrictions at all.
The crosswords don't require an account to play. The Fediverse element is to do with sharing puzzles, clues and leaderboards between servers.

cruciverb.lashman.live
Daily crossword puzzles in five sizes, four game modes, cryptic clues, multiplayer rooms, party games, and a puzzle editor. Self-hosted, federated, CC0.
Question for fediverse server admins and moderators — in the list of servers yours federates with, how would you prefer them to be sorted?
Now for something a bit different, a self-hostable Fediverse-compatible crossword server:
🌱 https://apps.lashman.live/cruciverb
There's a demo server at:
🌱 https://cruciverb.lashman.live
It's written by @lashman and is CC0 so it's totally free code with no restrictions at all.
The crosswords don't require an account to play. The Fediverse element is to do with sharing puzzles, clues and leaderboards between servers.

cruciverb.lashman.live
Daily crossword puzzles in five sizes, four game modes, cryptic clues, multiplayer rooms, party games, and a puzzle editor. Self-hosted, federated, CC0.
Question for fediverse server admins and moderators — in the list of servers yours federates with, how would you prefer them to be sorted?
The #Miscellany context for #ActivityPub, which documents a few widely-used terms, is now a Draft report of the #SocialCG:
https://swicg.github.io/miscellany/
/cc @socialcg
swicg.github.io
ok i just released a thing, over a year in the making 🙂
cruciverb - a daily crossword server that speaks ActivityPub
yes, really. your instance federates like a mastodon server: puzzles, clues and leaderboards flow between instances, and solves are signed so they follow you around the fedi
also: you can DRAW the letters. handwriting recognition runs entirely in your browser
no accounts, no ads, no tracking, CC0
apps.lashman.live
Self-hosted daily crosswords with handwriting input, a community clue pool, and ActivityPub federation. No ads, no tracking, no accounts. CC0 public domain.
I think I just figured out why the SocialCG prioritised work on encrypted private messaging for ActivityPub.
Decentralised DMs already give the fediverse an edge over the ATmosphere (eg BlueSky DMs are BS-only). If they're all E2EE as well, that puts us head and shoulders above anything the ATProtocol can currently handle.
(1/?)
The #Miscellany context for #ActivityPub, which documents a few widely-used terms, is now a Draft report of the #SocialCG:
https://swicg.github.io/miscellany/
/cc @socialcg
swicg.github.io
And lots of interesting things related to #ActivityPub are being incubated right now. Check out the meeting calendar at https://www.w3.org/groups/cg/socialcg/calendar/ , some of the resources and links we are collecting at https://activitypub.rocks, and activity on https://github.com/swicg.
github.com
W3C Social Web Incubator Community Group has 29 repositories available. Follow their code on GitHub.
ok i just released a thing, over a year in the making 🙂
cruciverb - a daily crossword server that speaks ActivityPub
yes, really. your instance federates like a mastodon server: puzzles, clues and leaderboards flow between instances, and solves are signed so they follow you around the fedi
also: you can DRAW the letters. handwriting recognition runs entirely in your browser
no accounts, no ads, no tracking, CC0
apps.lashman.live
Self-hosted daily crosswords with handwriting input, a community clue pool, and ActivityPub federation. No ads, no tracking, no accounts. CC0 public domain.
Has anyone made a plugin for #Mastodon or other #ActivityPub client or servers that allows someone to give a monetary #contribution for toots, posts or server host? Like next to a boost you have a button to donate, and you can just pick small amounts via whatever intermediary you'd configure. Perhaps the server host could configure it, perhaps the user when they set up the client.
Would be great if you could say "I could boost this, or favorite, or donate 25 cents to the poster or host".
ok i just released a thing, over a year in the making 🙂
cruciverb - a daily crossword server that speaks ActivityPub
yes, really. your instance federates like a mastodon server: puzzles, clues and leaderboards flow between instances, and solves are signed so they follow you around the fedi
also: you can DRAW the letters. handwriting recognition runs entirely in your browser
no accounts, no ads, no tracking, CC0
apps.lashman.live
Self-hosted daily crosswords with handwriting input, a community clue pool, and ActivityPub federation. No ads, no tracking, no accounts. CC0 public domain.
ok i just released a thing, over a year in the making 🙂
cruciverb - a daily crossword server that speaks ActivityPub
yes, really. your instance federates like a mastodon server: puzzles, clues and leaderboards flow between instances, and solves are signed so they follow you around the fedi
also: you can DRAW the letters. handwriting recognition runs entirely in your browser
no accounts, no ads, no tracking, CC0
apps.lashman.live
Self-hosted daily crosswords with handwriting input, a community clue pool, and ActivityPub federation. No ads, no tracking, no accounts. CC0 public domain.
Later today, we will be hosting a Social Web Community Group member orientation meeting if you are interested in ongoing standardization work related to #ActivityPub.
Bring your questions, if you are a new member of the community group, are considering joining, or are just curious what's going on with ActivityPub.
There is no need to be a W3C member or to register. For how to join and when in your timezone, go to https://www.w3.org/events/meetings/6fff6c3a-3a78-4119-a2ef-4d62d9996362/
w3.org
The World Wide Web Consortium (W3C) is an international community where Member organizations, a full-time staff, and the public work together to develop Web standards.
Later today, we will be hosting a Social Web Community Group member orientation meeting if you are interested in ongoing standardization work related to #ActivityPub.
Bring your questions, if you are a new member of the community group, are considering joining, or are just curious what's going on with ActivityPub.
There is no need to be a W3C member or to register. For how to join and when in your timezone, go to https://www.w3.org/events/meetings/6fff6c3a-3a78-4119-a2ef-4d62d9996362/
w3.org
The World Wide Web Consortium (W3C) is an international community where Member organizations, a full-time staff, and the public work together to develop Web standards.
And lots of interesting things related to #ActivityPub are being incubated right now. Check out the meeting calendar at https://www.w3.org/groups/cg/socialcg/calendar/ , some of the resources and links we are collecting at https://activitypub.rocks, and activity on https://github.com/swicg.
github.com
W3C Social Web Incubator Community Group has 29 repositories available. Follow their code on GitHub.
We haven't been posting very much here, but hope to do this again more regularly.
If you don't know, #ActivityPub -- the open protocol that lets you see this very post -- is a standard created under the auspices of the #w3c, the World-Wide-Web Consortium. This is the account of the Social Web Community Group in the W3C, which incubates additions and improvements to ActivityPub and related standards.
Operating #ActivityPub Undo side effects reversals is a bit of a pain when using a non versioned storage backend.
something something FEP about a "real time inbox" that signals that the actor should process this activity quickly? could be useful for inserting into some other queue? perhaps this could also be a batched inbox e.g. N number of activities can be delivered at once, like Matrix EDUs (iirc)?
do either of these concepts already exist as feps?
Oproep: kunnen jullie voorbeelden met me delen van interessante vormgeving op servers op #ActivityPub? Wat zijn de mogelijkheden om een NRC-server een beetje mooi te maken? Wat kan wel en wat kan niet? #NRCServer
something something FEP about a "real time inbox" that signals that the actor should process this activity quickly? could be useful for inserting into some other queue? perhaps this could also be a batched inbox e.g. N number of activities can be delivered at once, like Matrix EDUs (iirc)?
do either of these concepts already exist as feps?
@mario @movim Yes, when describing the status quo #ATproto is better in that regard (relative agnostic PDS with different 'views' on to the data). However I think that’s not necessarily inherent to #ActivityPub. It’s just how most (or basically all tbh) implementations work right now.
Loops, Pixelfed, Mastodon
@movim That said I listened to a presentation about #ActivityPub C2S by @django at #FOSSY and his efforts to create a general purpose client, which I guess would eventually also open the door to generic, frontend agnostic servers as well.
I guess the way it works right now is mostly for historical reason; That’s how #Mastodon did it and everyone followed. But it doesn’t have to be that way for ever.
Being able to migrate your posts along with followers to other Pixelfed servers is something we need to do, and will be a priority after I ship the new release, and mobile app later this month.
I say to other Pixelfed servers, because I'm not aware of any other software that would support this, but I do plan to write a FEP so we can support full account migration between software!
Earlier today I attended the #ActivityPub Trust & Safety Taskforce meeting!
It was my first time popping by, but I had a specific reason, namely a call to work on a specification for interaction controls based on various existing work. You may remember that I tried this before, earlier in the year, and it stalled due to a lack of time. So this time around I'm doing it during my work hours. Thanks @Lambo! 😉
Here's how I'm trying to get started: https://github.com/swicg/activitypub-trust-and-safety/issues/159#issuecomment-5348012095
github.com
In the last meeting (2026-08-19), we voted to start a workstream for a standards-track document for Interaction Controls, Replies and Quote Posts. These areas all over lap in varying ways, so would...
Earlier today I attended the #ActivityPub Trust & Safety Taskforce meeting!
It was my first time popping by, but I had a specific reason, namely a call to work on a specification for interaction controls based on various existing work. You may remember that I tried this before, earlier in the year, and it stalled due to a lack of time. So this time around I'm doing it during my work hours. Thanks @Lambo! 😉
Here's how I'm trying to get started: https://github.com/swicg/activitypub-trust-and-safety/issues/159#issuecomment-5348012095
github.com
In the last meeting (2026-08-19), we voted to start a workstream for a standards-track document for Interaction Controls, Replies and Quote Posts. These areas all over lap in varying ways, so would...
Earlier today I attended the #ActivityPub Trust & Safety Taskforce meeting!
It was my first time popping by, but I had a specific reason, namely a call to work on a specification for interaction controls based on various existing work. You may remember that I tried this before, earlier in the year, and it stalled due to a lack of time. So this time around I'm doing it during my work hours. Thanks @Lambo! 😉
Here's how I'm trying to get started: https://github.com/swicg/activitypub-trust-and-safety/issues/159#issuecomment-5348012095
github.com
In the last meeting (2026-08-19), we voted to start a workstream for a standards-track document for Interaction Controls, Replies and Quote Posts. These areas all over lap in varying ways, so would...
Earlier today I attended the #ActivityPub Trust & Safety Taskforce meeting!
It was my first time popping by, but I had a specific reason, namely a call to work on a specification for interaction controls based on various existing work. You may remember that I tried this before, earlier in the year, and it stalled due to a lack of time. So this time around I'm doing it during my work hours. Thanks @Lambo! 😉
Here's how I'm trying to get started: https://github.com/swicg/activitypub-trust-and-safety/issues/159#issuecomment-5348012095
github.com
In the last meeting (2026-08-19), we voted to start a workstream for a standards-track document for Interaction Controls, Replies and Quote Posts. These areas all over lap in varying ways, so would...
Ich fände das ja ganz praktisch, wenn man im #Fediverse für #ActivityPub nur einen einzigen Account bräuchte, den man dann auf
#Mastodon,
#Pixelfed,
#Peertube, #FediHood usw. benutzen kann.
@karsbehr@loma.ml @karsbehr@fedihood.social @karsbehr@pixel.tchncs.de
Has anyone else in #Aotearoa #NZ already tried #FediHood?
It looks like there are only two accounts in this contry (my trial one and another).
But I quite like the approach of using geographical proximity for interactions as well as using full end-to-end encryption for messaging. While all based on the #ActivityPub protocol.
It might also be a good platform for local organisations (e.g. sports teams, scouts, communities, neighbourhoods, etc.).
Has anyone else in #Aotearoa #NZ already tried #FediHood?
It looks like there are only two accounts in this contry (my trial one and another).
But I quite like the approach of using geographical proximity for interactions as well as using full end-to-end encryption for messaging. While all based on the #ActivityPub protocol.
It might also be a good platform for local organisations (e.g. sports teams, scouts, communities, neighbourhoods, etc.).
Coucou! This is just a first post announcing Nuages to the world.
Nuages is an ActivityPub app, connecting to your favourite servers that support the Client to Server API.
Follow us to get our latest updates, we’ll be officially launching the app in the coming weeks 😉
#ActivityPub #c2s #ActivityPubAPI #Fediverse
#activitypub #ActivityPubAPI #c2s #fediversemediaformat.org
Do you yearn for a user powered social network? A social space free from constant Ads, Sponsored and AI generated slop? The Fediverse 1 is a network where you c
Coucou! This is just a first post announcing Nuages to the world.
Nuages is an ActivityPub app, connecting to your favourite servers that support the Client to Server API.
Follow us to get our latest updates, we’ll be officially launching the app in the coming weeks 😉
#ActivityPub #c2s #ActivityPubAPI #Fediverse
#activitypub #ActivityPubAPI #c2s #fediversemediaformat.org
Do you yearn for a user powered social network? A social space free from constant Ads, Sponsored and AI generated slop? The Fediverse 1 is a network where you c
Here's v0.3.0 of ties, the federated bookmark manager, with a new light mode, flexible bookmark editing tools, and a critical security fix!
If you're using ties, please update at your earliest convenience. If you're not using it yet, now's a good time to try it out!
We are now more than 300 users on #FediHood. The project is stable enough for me to invite you to try it.
It is a software running on #ActivityPub and using #OSM.
The idea? Discover people near you or all around the world, and filter by topic.
It doesn't use GPS, you pick the town. The app warns you if your city is too small and suggests a bigger one nearby. Bonus: it supports #E2EE DMs.
Don't hesitate to share. Thank you!
fedihood.social
Discover and discuss with people near you, by topic. Built on ActivityPub and OpenStreetMap.
Decided to hop on https://fedihood.social and see what it's like. Seems like a fun concept! Those of you who feel comfortable joining totally should (you can use a pseudonymous acct if you like).
It seems to be largely lacking in users still. I would love to see this thing take off in a real way!
fedihood.social
Discover and discuss with people near you, by topic. Built on ActivityPub and OpenStreetMap.
I feel the hidden question of the #poll is: Given the #technology base exists, can you just code against it at will? I.e. the tech #ethics question is asked. Where on one end of the spectrum we find the techbro / fossbro, and on the other end of the spectrum we are still learning what we should have there as an alternative to make a social web be social and keep it that way. As "social web" technology by itself offers zero guarantee of that.
Decided to hop on https://fedihood.social and see what it's like. Seems like a fun concept! Those of you who feel comfortable joining totally should (you can use a pseudonymous acct if you like).
It seems to be largely lacking in users still. I would love to see this thing take off in a real way!
fedihood.social
Discover and discuss with people near you, by topic. Built on ActivityPub and OpenStreetMap.
We are now more than 300 users on #FediHood. The project is stable enough for me to invite you to try it.
It is a software running on #ActivityPub and using #OSM.
The idea? Discover people near you or all around the world, and filter by topic.
It doesn't use GPS, you pick the town. The app warns you if your city is too small and suggests a bigger one nearby. Bonus: it supports #E2EE DMs.
Don't hesitate to share. Thank you!
fedihood.social
Discover and discuss with people near you, by topic. Built on ActivityPub and OpenStreetMap.
We are now more than 300 users on #FediHood. The project is stable enough for me to invite you to try it.
It is a software running on #ActivityPub and using #OSM.
The idea? Discover people near you or all around the world, and filter by topic.
It doesn't use GPS, you pick the town. The app warns you if your city is too small and suggests a bigger one nearby. Bonus: it supports #E2EE DMs.
Don't hesitate to share. Thank you!
fedihood.social
Discover and discuss with people near you, by topic. Built on ActivityPub and OpenStreetMap.
Is it OK to write code that does new things on the Fediverse?
We are now more than 300 users on #FediHood. The project is stable enough for me to invite you to try it.
It is a software running on #ActivityPub and using #OSM.
The idea? Discover people near you or all around the world, and filter by topic.
It doesn't use GPS, you pick the town. The app warns you if your city is too small and suggests a bigger one nearby. Bonus: it supports #E2EE DMs.
Don't hesitate to share. Thank you!
fedihood.social
Discover and discuss with people near you, by topic. Built on ActivityPub and OpenStreetMap.
We are now more than 300 users on #FediHood. The project is stable enough for me to invite you to try it.
It is a software running on #ActivityPub and using #OSM.
The idea? Discover people near you or all around the world, and filter by topic.
It doesn't use GPS, you pick the town. The app warns you if your city is too small and suggests a bigger one nearby. Bonus: it supports #E2EE DMs.
Don't hesitate to share. Thank you!
fedihood.social
Discover and discuss with people near you, by topic. Built on ActivityPub and OpenStreetMap.
Activity Pub bot instance (Mastodon-compatible) is now in v1.2. Mostly security hardening, some bug fixes + tests and Docker + nginx to deploy along previously available Apache.
Want to run your own bots in Fediverse? This is your quick & easy way to launch.
https://github.com/nekromoff/mastodon-bots
#activityPub #mastodon #mastolivre #mastoadmin #bots #bot #opensource #webdev #php #software
github.com
ActivityPub / Mastodon compatible instance to quickly create and deploy multiple bots (identities) that can be controlled via simple API. - nekromoff/mastodon-bots
Activity Pub bot instance (Mastodon-compatible) is now in v1.2. Mostly security hardening, some bug fixes + tests and Docker + nginx to deploy along previously available Apache.
Want to run your own bots in Fediverse? This is your quick & easy way to launch.
https://github.com/nekromoff/mastodon-bots
#activityPub #mastodon #mastolivre #mastoadmin #bots #bot #opensource #webdev #php #software
github.com
ActivityPub / Mastodon compatible instance to quickly create and deploy multiple bots (identities) that can be controlled via simple API. - nekromoff/mastodon-bots
Servers
- Akkoma v2026.08
- WriteFreely v0.17.2
- Wafrn v2026.08.03
- Mitra v5.9.0
- Ktistec v3.11.0
- Hollo v0.9.11
- Mastodon v4.6.6
- Forte v26.8.11
- gathio v1.6.4
- ActivityPub for WordPress v9.2.2
- NeoDB v0.17.6
- NodeBB v4.15.0
- Trunk & Tidbits, July 2026 (Mastodon)
Clients
- Aria v1.5.11
- Snowdrop: Multiplatform client for Mastodon API compatable Fediverse software
Tools and Plugins
For developers
- Fedipub v0.9.0
- Library progress report - August 2026 (GoActivityPub)
Protocol
- FEP-e4de: Closing Registration on Unattended Servers
- FEP-633c: Guardians
Articles
- Deploying your own Vernissage instance
-----
#WeekInFediverse #Fediverse #ActivityPub
Previous edition: https://mitra.social/objects/019fde3b-670e-7793-9180-6d8d0ddca40a
mitra.social
Week in Fediverse 2026-08-07 - Post by @weekinfediverse
I often see that question about the way I use #E2EE over #ActivityPub.
#FediHood and #HolosSocial do not use the end-to-end encryption #Mastodon is working on. They are going for MLS over ActivityPub. I use the Signal Protocol, already working between Holos and FediHood, which I both maintain. Once MLS is available, I'll happily switch.
But to be clear: it works perfectly with the Signal Protocol. The switch would only be for compatibility.
Feedback about #FediHood is greatly appreciated. This is a small, unpretentious #Fediverse project. But it seems that the idea of linking #OpenStreetMap to discover content near you was something missing here. It was designed to never use a GPS. It warns you when your city is too small and suggests the biggest one around. Topics and language allow you to get tailored content. I will introduce the #ActivityPub Event used by #Mobilizon for the event topic.
Thank you for using and sharing.
Feedback about #FediHood is greatly appreciated. This is a small, unpretentious #Fediverse project. But it seems that the idea of linking #OpenStreetMap to discover content near you was something missing here. It was designed to never use a GPS. It warns you when your city is too small and suggests the biggest one around. Topics and language allow you to get tailored content. I will introduce the #ActivityPub Event used by #Mobilizon for the event topic.
Thank you for using and sharing.