Skip to main content

Create an ActivityPub server


ActivityPub is a W3C Recommendation that defines a protocol that provides a client to server API for creating, updating and deleting content, as well as delivering notifications and content.

In this guide you will

  • Setup a SemApps-powered ActivityPub server;
  • Create an actor;
  • Make this actor communicate with another Mastodon actor.


You need to have NodeJS installed on your computer (use NodeJS version 14).

You also need Docker and docker-compose installed on your machine.

Setup a new Moleculer project

First install the moleculer-cli tool globally.

npm install -g moleculer-cli

Then initialize a new project based on this template with this command:

moleculer init assemblee-virtuelle/semapps-template-activitypub my-activitypub-server

Choose Yes to all questions:

? Do you need a local instance of Jena Fuseki (with Docker)? Yes
Create 'my-activitypub-server' folder...
? Would you like to run 'npm install'? Yes

Running npm install...

You can now go to the newly-created directory:

cd my-activitypub-server

Launch your local Jena Fuseki instance

Jena Fuseki is a semantic triple store. It is where your app's data will be stored.

docker-compose up

Jena Fuseki is now available at the URL http://localhost:3030.

By default the login is admin and the password is also admin.

There should be no dataset yet. They will be created when launching Moleculer.

Create a HTTPS tunnel to your localhost

Since we will want to communicate with a Mastodon instance later on this guide, we need to provide a publicly-accessible server, and we need it to handle HTTPS.

We recommend two reverse proxies for this:

  • Ngrok which has many options and an excellent web interface for inspection, but unfortunately the free version generates a different domain name on every session, which can make federated testing more difficult.
  • Loophole which has fewer options but allow to set custom subdomains for free.

For this guide, we will use Loophole and create a reverse proxy with the following configuration:

This will create a publicly-accessible URL like which will point to port 3000 of your local computer.

Now, to ensure all the URLs generated by your server will use this domain, edit the SEMAPPS_HOME_URL variable in the .env file at the root of your repository:


Make sure to use the HTTPS URL, and not the HTTP one, it is required by the ActivityPub protocol!

Run Moleculer in dev mode

All configurations are finished, you can now launch the server:

npm run dev

Your instance of SemApps is now available at (the local URL http://localhost:3000 will not work).

You can look up at the 3 LDP containers that Moleculer created in the /activities, /actors and /objects path.

Create an ActivityPub actor

You will now create an user, by POSTing to the /auth/signup endpoint with a tool like Insomnia, Postman or the RESTClient add-on for Firefox.

POST /actors HTTP/1.1
Content-Type: application/json

"username": "alice",
"email": "",
"password": "test"

In return, you will receive a JSON payload which will look like this:

"token": "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJ3ZWJJZCI6Imh0dHBzOi8vYWxpY2UubG9vcGhvbGUuc2l0ZS9hY3RvcnMvYWxpY2UiLCJpYXQiOjE2NjU5OTY2MDJ9.KdGNRIwl2da2Vk15sEJ7OcqHi84hIbFTCiDs8OLQln_HvoO5B4NqEVZbsr92lBQiDE9lr9FJMPlPPy1w6_iT9eIIDqj8H3Mjlir6Xti2aleRM4RT3EN0yPxBHkTSVdX8hgrRdMZzMfpFVAfAl_TQCRkChga5XZ3ozW5_oPy3NqgYwl_yARUbzS45VZnaW8CMIrsarA8sPbgHFO6Kx5ijvWGZdeaLO7QxEMr9uHN32DT3jP_WClAacIV22sbf-N_0hvm9ckWDcbaYctDBZny6E9jp56yaJ6oRqEk-ue8Hv7lh6rb9omS6zQcEPzL3ob67mMSwxufMrX2zIHDthnD_n5ozhOizBbJxzkr0XUxGdw4ZwQbzhi2ndaP0T9vd3tErCtGQOuTraO1E-59CpurFNdDIg-vRrGAxCGrv5Rytwr1k9uWhsMThBFWixCMSbxUvXDHpkHCWHJI_VXDCgm203SCc-R8V3T4zRfOMX_ga9Pw0gn4RWc8DbjqOHVMrfOabw0faeZlOAy9C4qvNJ3RWoA6TbG8HVuBgeBjkCcfO04EcxqGPSzLAfBblili1QC7ayon8fS51aIJASwMtTR3nqFXxEr3y3ZZRhWZtVm2K8T-YyeEi14cBEoS-NhoNd5jDzX6dCkoP7y7g0etXufICieNBu5LvvKijcR-GG_UHMB0",
"webId": "",
"newUser": true

Copy the token as you will need it to authenticate your future requests.

The webId property is the unique URI of the alice actor. Try to fetch it:

"@context": ["", ""],
"id": "",
"type": "Person",
"inbox": "",
"publicKey": {
"owner": "",
"publicKeyPem": "-----BEGIN PUBLIC KEY-----\nMIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEA0S4/wAxO2CJ0kxW2PdD7\nbpEURoXbezvAJYd1QneBSnBGqth/LhHD/RtX3/70B7GWghXuQpfySsIfSfCh9+f6\nSqmD7Hv93wXGsoSeDztu/Ss6vrVksxPf+mzz8lmMMw65wz+zE7rl95MQA6XputNY\nFIYcj2y5V4MLwrEjTeOEhRSmyM8EPmCPC/MKm20ZUpO0iNSOvvS9xc09ObcHcgem\nrohCrPNzqwWijjRRoY6qZ6xv9wSqBPmvmjbclW/Z9M5LwTD81iLwTCbFGtjO9pwY\nBntL+NNaA4GnRgr0XPmDzX9J5RdCiQc8lyyLFv/X/VWoAqxxy8IwyGwyP4O6OdhS\nqCaeLlFK8E4G0ZcEPj7vkubsvbNARybVHxTvGpm/oPywOjns3Zs/06m4iXD8OnEG\nWt3RO74U8L1sylQ+n8ww1WB2rCZCA8PLUrj5PeauDoWnpEjd7Fn17qjEBPbPuyOC\n+Qm5ZutxTCQdOyI3cpv+XIRuACcjS3hW6N7ML1pCtKHTNpfdSaiVxavxFJUIObur\ntestqyzbuncrSKb1c40ujFvizGkDdsuNtZaxMbZF7wXc+YQU+OhTRddhBlUVmgJy\nyN10CUh7TyMMOnin/YIZFscL9LNvN5XRWTqMSQBGo2TRNfWYNhs5Mhp8UKHitRXW\nr5iODz+M2V92gSBcNbhmp30CAwEAAQ==\n-----END PUBLIC KEY-----\n"
"followers": "",
"following": "",
"liked": "",
"outbox": "",
"preferredUsername": "alice"

The ActivityPub service has automatically added all the required properties for actors: the outbox, the inbox, the list of followers and following, as well as a publicKey object which will be used to verify messages sent by this actor.


If you look inside the /actors subdirectory, you should see two files: alice.key and The first one is the private key used to sign messages; it should never leave your server. The second is the public key that we saw just before.

Create a Mastodon user

Mastodon is federated social network to exchange Twitter-like messages. Unlike Twitter, Mastodon can be installed on many different servers, which use the ActivityPub protocol to communicate with each others. And since ActivityPub is an universal protocol, many different softwares can exchange with Mastodon instances.

This is what we will do now: exchange information with a Mastodon user. To do that, create an account on any of the instances that you can find on the Mastodon homepage.

For this guide, we will create an user bob on the instance.

Make the two actors follow each others

Send a POST request to Alice's outbox with the following information. Please note that the JWT token retrieved after the signup is provided through the Authorization header. If you fail to do that, you will get a 403 Forbidden error.

POST /actors/alice/outbox HTTP/1.1
Content-Type: application/ld+json
Authorization: Bearer eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJ3ZWJJZCI6Imh0dHBzOi8vYWxpY2UubG9vcGhvbGUuc2l0ZS9h...

"@context": "",
"actor": "",
"type": "Follow",
"object": "",
"to": ""

We use to identify Bob's account, because that's the standard URL for Mastodon actors. However we could have found this same information through the Webfinger protocol. If you fetch this URL with an Accept: application/json header, you should receive all the information about the user Bob.

Follow activities are usually followed by an Accept activity to confirm that the remote actor has accepted the request. If the magic happened, you should see it on Alice's inbox ( Furthermore, Bob should appear in Alice's following collection:

If you now go to Bob's Mastodon account, you should see a notification that the user wants to follow him.

Click on the icon on the right to follow her back. Again, you can check that the messages is received in Alice's inbox, and that Bob appear in Alice's followers (

Send a Note to your followers

You're almost there ! Since the two users follow each others, you can post a message as Alice, and Bob should receive it on his Mastodon feed.

POST /actors/alice/outbox HTTP/1.1
Content-Type: application/ld+json

"@context": "",
"type": "Note",
"attributedTo": "",
"content": "Hello to all my followers !",
"to": [ "" ]

The message is not sent directly to Bob (otherwise it would appear as a direct message), but to all of Alice's followers. If everything went well, this message should appear right at the top of Bob's Mastodon feed !


Mastodon, being a Twitter "clone", only accept two types of objects: Note and Question. Other types of ActivityPub objects are converted but you cannot be sure of the result. For more information, see this page.

Much more could be done, but with this little guide, we hope you had a taste of what it is like to create your very own ActivityPub server !