No description
  • Python 92.4%
  • HTML 7.6%
Find a file
Erick Ruiz de Chavez 07517adb6b Tie actor URI to RELAY_ACTOR_NAME so identity can be rotated
Actor URI was a hardcoded bare /actor regardless of RELAY_ACTOR_NAME, so
that config var only affected the WebFinger acct: form and
preferredUsername. Changing it did nothing to actually change the actor's
identity. Now actor_uri is https://{RELAY_DOMAIN}/actor/{RELAY_ACTOR_NAME},
so changing RELAY_ACTOR_NAME yields a genuinely new actor URI/keyId — the
escape hatch for when a peer server caches a stale key for us that
survives unfollow/refollow (hit this with GoToSocial after wiping our
keypair during development; the old actor path is now abandoned in favor
of a fresh one).

Assisted-By: Claude <noreply@anthropic.com>
2026-07-16 15:12:50 +00:00
app Tie actor URI to RELAY_ACTOR_NAME so identity can be rotated 2026-07-16 15:12:50 +00:00
.env.example Initial implementation of self-hosted ActivityPub firehose relay 2026-07-16 11:41:08 +00:00
.gitignore Initial implementation of self-hosted ActivityPub firehose relay 2026-07-16 11:41:08 +00:00
CLAUDE.md Tie actor URI to RELAY_ACTOR_NAME so identity can be rotated 2026-07-16 15:12:50 +00:00
compose.yaml Initial implementation of self-hosted ActivityPub firehose relay 2026-07-16 11:41:08 +00:00
pyproject.toml Initial implementation of self-hosted ActivityPub firehose relay 2026-07-16 11:41:08 +00:00
README.md Tie actor URI to RELAY_ACTOR_NAME so identity can be rotated 2026-07-16 15:12:50 +00:00
uv.lock Initial implementation of self-hosted ActivityPub firehose relay 2026-07-16 11:41:08 +00:00

relay

A small, self-hosted, manually-gated ActivityPub relay. It consumes relay.fedi.buzz's public Mastodon-streaming firehose (https://fedi.buzz/api/v1/streaming/public) and rebroadcasts every post it sees as an Announce activity to whichever servers you've approved as followers — nothing is filtered by tag, instance, or language.

Unlike old ActivityRelay-style (LitePub) relays, this one uses the same one-way, bare-Announce-object pattern as buzzrelay/AodeRelay/rel.re, which is what makes it work with GoToSocial.

How it works

  • Peers (e.g. your GoToSocial instance) send a Follow to this relay's actor. It shows up as pending in the admin UI — nothing is auto-accepted.
  • You manually Accept or Reject each request at /admin/requests.
  • Once accepted, every post from the firehose is Announced to that follower's inbox (except posts originating from that follower's own domain).
  • Remove a follower any time at /admin/followers — no Undo needed on our side since we never Follow back.

Running

cp .env.example .env   # set RELAY_DOMAIN to your real public hostname
docker compose up -d

No Dockerfile/image build step — compose.yaml runs uv's own image directly against the source bind-mounted from this directory (uv sync happens on container start), same pattern as the poster project.

The relay actor will be reachable at https://$RELAY_DOMAIN/actor/$RELAY_ACTOR_NAME (default RELAY_ACTOR_NAME=relay, so https://$RELAY_DOMAIN/actor/relay). Point GoToSocial's Admin → Federation → relay-subscribe flow at that URL (or the WebFinger form acct:relay@$RELAY_DOMAIN), then approve the resulting follow request in /admin/requests.

The actor path includes RELAY_ACTOR_NAME on purpose: if a peer server ever caches a stale public key for us (e.g. after we regenerate our keypair) and won't drop it even after unfollow/refollow, changing RELAY_ACTOR_NAME gives the relay a brand new actor URI/keyId, forcing a completely fresh discovery — at the cost of the peer needing to redo their subscription.

Important — proxy exposure: /actor/*, /.well-known/webfinger, and /inbox must stay publicly reachable with no auth (remote servers hit them directly). Everything under /admin/* and / should sit behind your reverse proxy's auth (e.g. Pangolin) — there is no app-level login.

Local dev

uv run uvicorn app.main:app --reload

Data

Everything (follower state + the relay's own signing keypair) lives in a single SQLite file at $DATA_DIR/relay.db (WAL mode), under ./data by default — since the whole project directory is bind-mounted into the container, this is just a regular directory on the host. That's the only thing you need to back up.