Skip to content


Switch branches/tags

Name already in use

A tag already exists with the provided branch name. Many Git commands accept both tag and branch names, so creating this branch may cause unexpected behavior. Are you sure you want to create this branch?

Latest commit


Git stats


Failed to load latest commit information.
Latest commit message
Commit time

This is the codebase for, an ActivityPub server with a single hardcoded 'King' service actor that acts as a chess arbiter.

This code is MIT-licensed, see

Not a requirement, but if you use this code for anything, I'd love to hear about it! Send me a toot:

How to run

To run the app, you need Node.js (at least 16.x), Yarn, and PostgreSQL (at least 9.6).

First, install the dependencies and build the TypeScript code:

yarn install
yarn build

Next, you should configure the app.

Configuration is provided through environment variables, which can also be provided in a .env file. The defaults should work for development. The following variables are used (with their defaults):

  • APP_SCHEME="http"

    The scheme used to access the app. The app only talks plain HTTP, but this should be set to https when behind a reverse proxy providing HTTPS.

  • APP_DOMAIN="localhost:5080"

    The domain used to access the app. You should set up a virtual host matching this, and have it proxy to the app.


    Profile URL of the server admin, publicly accessible through the NodeInfo API.


    Email of the server admin, publicly accessible through the NodeInfo API.

  • APP_KEY_FILE="signing-key"

    Filename of the RSA private key (in PEM format) used to sign activities for the 'King' actor. A matching file with a .pub suffix MUST be present, containing the RSA public key (also in PEM format).

    Use tools/ <filename> to generate a pair.


    The secret used to sign public data generated by the app that must later be verified. The default MUST NOT ever be used in a public instance.

    Currently, this is only used to sign image URLs, so that they cannot be tampered with.

    One can be generated with, for example: head -c 63 /dev/random | base64

  • NODE_ENV="development"

    One of development or production.

    It's important to set this to production on a live instance. When not in production, the app will not enforce, but only warn about security issues. (E.g. invalid request signatures, and federation over non-HTTPS.)

    This variable may also change various behaviour in library dependencies.

  • PORT="5080"

    The TCP port the app will listen on for HTTP requests.

    The app will always bind on all interfaces, and MUST be properly firewalled in a public instance.

  • PostgreSQL connection variables follow the standard set defined at:

Now generate the signing key:

./tools/ signing-key

Make sure the database exists in PostgreSQL, then create the schema:

yarn migrate up

The app can now be started with:

yarn start

Debug logging is provided by the debug module. For development,'s own debug logging can be enabled using, for example:

DEBUG='chess:*' yarn start


With the app running locally in development using default settings, you can run an automated test that plays a short game:


App structure

The server.js entry point loads configuration, then starts the two main tasks of the app:

  • src/front/ contains the frontend code handling HTTP requests.
  • src/deliver/ contains the outbox delivery queue processing code.

These could easily be split into separate processes, and could also individually be scaled horizontally, because all state lives in PostgreSQL. In practice, that kind of scale is not reached, so things are kept simple and single-process for now.

The codebase heavily uses async/await, with the Koa framework for its frontend.

Instances of library dependencies and our own services (such as Koa, the PostgreSQL client, etc.) live on an app object. This structure can be best seen in src/shared/createApp.js and the index.js files of each task directory. This is basically poor-man's dependency injection; there's no automated system to resolve the dependencies between objects, we just create them in the order we know works.

The codebase sticks to ActivityPub naming. Notably, this means: not 'toots', but 'notes'.

When interacting with the bot, there are roughly 6 steps that take place:

  1. The user looks up the bot by name.
  2. The user publishes a note.
  3. The user's instance delivers it to the bot's and others' instances.
  4. The bot acts on the note and publishes a reply.
  5. We deliver the reply to the users' instances.
  6. The users views our reply.

Step 1 involves an optional Webfinger request, and fetching our Actor object. Both of these are implemented in src/front/actor.js. In a server implementation like Mastodon, these would be dynamic, because there's an account system. But in, a single account is simply hardcoded.

Step 3 is when we receive a signed request POST /inbox. This is implemented in src/front/inbox.js. The signature is verified by code in src/shared/signing.js, then dispatched as an internal event noteCreated.

Step 4 starts when the event is picked up in src/front/dispatch.js. Here, the note is parsed and methods are called on src/front/game.js and src/front/challengeBoard.js, which are roughly our controllers when thinking in MVC. When these decide a reply needs to be sent, they call createObject() of src/front/outbox.js, which queues deliveries to user inboxes.

Step 5 is where outbox deliveries are picked up, implemented in src/deliver/deliver.js. Each delivery is a two-step process, where first the addressed Actor is fetched to discover the inbox, then the note is actually delivered to the inbox. These are implemented as essentially two separate jobs folded into a single table deliveries. Keeping these separate allows combining deliveries that have the same shared inbox.

Step 6 is of note, because images are lazily rendered. A signed image URL is generated in step 4, and when requested, the image is fetched from cache or rendered on the spot. This code lives in src/front/draw.js.

The ActivityPub objects are all accessible by browser as HTML. This is important, because the actor URL and note URLs are often visible to the user. renders templates for these when accessed through a browser, as well as providing various other browser-only pages in src/front/misc.js. The actual template files are EJS files, found in assets/tmpl/.


Challenge someone to a game of chess using toots!