Skip to content

@indiepub/activitypub

ActivityPub federation and Mastodon POSSE. When configured, your site becomes a fully-fledged ActivityPub actor — Fediverse users can follow your site directly from Mastodon, Misskey, Pixelfed, and other compatible clients.

Configuration

indiepub({
syndication: {
mastodon: { instance: 'mastodon.social', handle: 'yourhandle' },
},
})

Required env vars: MASTODON_ACCESS_TOKEN, ACTIVITYPUB_PRIVATE_KEY, ACTIVITYPUB_PUBLIC_KEY

How it works

WebFinger

GET /.well-known/webfinger?resource=acct:yourhandle@yourdomain.com returns a JRD document pointing to your Actor URL. This is how Fediverse clients discover your identity.

Actor

GET /actor returns an ActivityPub Person document with your public key for HTTP Signature verification. Browser requests are redirected to your site home.

Inbox

POST /actor/inbox handles incoming ActivityPub activities:

  • Follow — stores the follower in ap_followers, fetches their inbox URL, sends a signed Accept activity back
  • Undo(Follow) — removes the follower from ap_followers

Other activity types are accepted silently (202 response).

Delivery

When you publish a new post, deliverActivity() fans out a Create(Note) or Create(Article) activity to all follower inboxes in parallel using Promise.allSettled. Delivery failures for individual followers are non-fatal.

HTTP Signatures

All outgoing requests (Accept activities, post delivery) are signed with RSA-SHA256 per the HTTP Signatures spec as expected by Mastodon. The Digest header is also included for POST requests.

Generating a key pair

Terminal window
openssl genrsa 2048 | tee private.pem | openssl rsa -pubout -out public.pem

Add to your .dev.vars and Cloudflare Pages secrets:

ACTIVITYPUB_PRIVATE_KEY="-----BEGIN PRIVATE KEY-----
<contents of private.pem>
-----END PRIVATE KEY-----"
ACTIVITYPUB_PUBLIC_KEY="-----BEGIN PUBLIC KEY-----
<contents of public.pem>
-----END PUBLIC KEY-----"

Mastodon POSSE API

syndicateToMastodon(entry, canonicalUrl, instance, accessToken) creates a status on your Mastodon account. Status text respects the 500-character limit.

import { syndicateToMastodon } from '@indiepub/activitypub';
const result = await syndicateToMastodon(entry, 'https://yourdomain.com/posts/slug', 'mastodon.social', token);
// result: { id: string, url: string }

Followers collection

GET /actor/followers returns an OrderedCollection of all follower Actor URLs, read from the ap_followers D1 table.