Yet another powerful customizable ActivityPub relay server written in Go. https://relay.argentwolf.org
  • Go 85.3%
  • Python 10.5%
  • Shell 1.7%
  • JavaScript 0.9%
  • HTML 0.8%
  • Other 0.8%
Find a file
alan 76e07d55d4
All checks were successful
Container / Container and Caddy validation (push) Successful in 1m6s
Debian Package / Debian package validation (push) Successful in 1m41s
Test / Go, Redis, and Python validation (push) Successful in 2m35s
Merge pull request 'Apply RC3 finding fixes' (#6) from release/rc3-findings-for-rc4 into master
Reviewed-on: #6
2026-10-05 21:32:58 +00:00
.forgejo/workflows Prepare Activity-Relay 3.0.1 release 2026-09-30 14:20:40 -04:00
.github Prepare Activity-Relay 3.0 stable defaults and release docs 2026-09-01 21:47:50 -04:00
api CI failure corrections 2026-10-05 17:05:43 -04:00
contrib Apply RC3 finding fixes 2026-10-05 16:00:25 -04:00
control Harden follower state validation and relay fan-out 2026-09-30 13:15:34 -04:00
debian Apply RC3 finding fixes 2026-10-05 16:00:25 -04:00
deliver Wire destination signature negotiation runtime 2026-08-02 15:55:49 -04:00
docs Apply RC3 finding fixes 2026-10-05 16:00:25 -04:00
internal Fix test regression 2026-10-05 17:17:47 -04:00
misc Add configurable public address distribution policy 2026-08-03 18:21:15 -04:00
models Hardening 2026-10-04 22:31:06 -04:00
scripts/release Prepare Activity-Relay 3.0 stable defaults and release docs 2026-09-01 21:47:50 -04:00
testdata Apply RC3 finding fixes 2026-10-05 16:00:25 -04:00
.dockerignore Make relay frontend optional and refresh release documentation 2026-07-27 09:06:08 -04:00
.env.example Prepare Activity-Relay 3.0.1 release 2026-09-30 14:20:40 -04:00
.gitignore Add explicit summary scheduling and resilient mail delivery 2026-07-27 17:22:25 -04:00
AGENTS.md Hardening 2026-10-04 22:31:06 -04:00
ARCHITECTURE.md Harden follower state validation and relay fan-out 2026-09-30 13:15:34 -04:00
CHANGELOG.md Apply RC3 finding fixes 2026-10-05 16:00:25 -04:00
compose.build.yml Improve low-memory builds and configurable website paths 2026-07-27 09:49:37 -04:00
compose.yml Prepare Activity-Relay 3.0.0 stable release 2026-09-02 23:20:42 -04:00
config.yml.example Apply RC3 finding fixes 2026-10-05 16:00:25 -04:00
Dockerfile Apply RC3 finding fixes 2026-10-05 16:00:25 -04:00
go.mod Add manual directory lifecycle commands 2026-08-05 09:06:48 -04:00
go.sum Add RFC 9421 and RFC 9530 signing core 2026-08-02 10:40:25 -04:00
LICENCE Version 0.0.1 2018-11-07 02:31:24 +09:00
main.go Hardening 2026-10-04 22:31:06 -04:00
main_signal_test.go Add automatic directory lifecycle scheduler 2026-08-05 14:39:52 -04:00
readme.md Hardening 2026-10-04 22:31:06 -04:00
TODO.md Hardening 2026-10-04 22:31:06 -04:00

Activity Relay Server

A maintained and deployable ActivityPub relay written in Go

GitHub mirror CI

0f3f8ae2-a325-4a2e-9ace-b30fbc79230c

Note

This repository is a maintained fork of yukimochi/Activity-Relay, based on upstream release v2.0.10.

The authoritative source repository is forgejo.argentwolf.org/alan/activity-relay. github.com/thystra/Activity-Relay is a downstream public mirror with independent validation. The public Go module path remains github.com/thystra/Activity-Relay.

Highlights

Compared with the upstream baseline, this fork includes:

  • Relay-signed actor and canonical-object GET requests for Mastodon authorized-fetch and secure-mode interoperability.
  • RFC 9421 HTTP Message Signatures and RFC 9530 Content-Digest with strict inbound verification and explicit legacy, rfc9421, and destination-aware dual outbound modes; 3.0 uses dual as the omitted/default policy so new deployments retain legacy compatibility while learning modern capability.
  • An opt-in Activity-Relay Directory client with Protocol v1 compatibility, negotiated Protocol v2 descriptive-profile synchronization, manual lifecycle commands, and a fenced API-process scheduler for registration and heartbeat.
  • An ActivityStreams Application relay profile recognized by current Friendica discovery without rotating the actor key or changing endpoints.
  • Server-actor follow and unfollow compatibility for NodeBB, Friendica, LitePub, and other Application or Service actors, while preserving /relay and /friendica legacy paths.
  • Acceptance of valid HTTP-signed public activities from unsubscribed publishers, including WordPress ActivityPub sites.
  • Publisher first-seen, last-seen, activity-type, and accepted-activity counters.
  • /status.json schema version 5 with:
    • public_address_distribution_policy and its human-readable label;
    • connected_instances: all unique participating domains;
    • receiving_instances: domains that receive relay fan-out, with current delivery-health timestamps and counters;
    • publishers: observed sending domains and their roles.
  • A tested Redis-backed fan-out pipeline with bounded queue and response controls, leased in-flight task claims, and at-least-once recovery after abrupt worker termination.
  • Complete follower-state validation that prevents stale mutual-follow updates from creating partial Redis records and prevents one malformed receiver from blocking healthy fan-out targets.
  • Non-configurable relay-reflection protection that excludes the supplying relay and deduplicates relay-authored wrappers by a hashed canonical activity reference for the bounded delivery-retention horizon.
  • Multi-architecture linux/amd64 and linux/arm64 container releases on GHCR.
  • Native Ubuntu 24.04 amd64 Debian packages with systemd units, a dedicated Redis instance, operational resource monitoring, and upgrade-safe identity preservation.
  • An optional generated public website. Operators may use it, replace it, redirect it, or serve no frontend at all.

See CHANGELOG.md for release details.

Module and compatibility

The maintained fork uses:

github.com/thystra/Activity-Relay

Existing ActivityPub endpoints, YAML settings, environment variables, Redis state, and control commands remain compatible unless a release explicitly documents otherwise. Asynchronous work retains the github.com/RichardKnop/machinery/v2 import path and uses a Go replace directive pinned to the Redis-only github.com/thystra/machinery/v2 fork at v2.0.17-0.20260730204902-5efae3f700cd. The migration preserves the existing relay queue, delayed retries, task-signature JSON, and result-state encoding.

The go-redis worker moves ready work atomically into leased in-flight claim records and acknowledges a claim only after processing succeeds. Expired claims are recovered to the ready queue. This prevents silent task loss during abrupt worker or host failure, but it is intentionally at-least-once: a remote side effect may be repeated when the worker dies after remote success but before local completion and acknowledgement.

Requirements

Depending on the installation method:

  • Docker and Docker Compose for container deployment;
  • Ubuntu 24.04 or a compatible Debian-based system for the native package;
  • Go and Redis for source builds;
  • Python 3 for resource-guard tooling and optional website generation;
  • Nginx, Apache, Caddy, or another reverse proxy for a public deployment.

Installation

Container deployment

Stable releases and release candidates are published to:

ghcr.io/thystra/activity-relay

Copy the examples:

cp .env.example .env
cp config.yml.example config.yml

Set the stable release image in .env:

ACTIVITY_RELAY_IMAGE=ghcr.io/thystra/activity-relay:3.0.1

Release candidates use their complete -rcN tag; prereleases do not move latest, major, or major/minor stable tags.

Generate the actor identity once:

docker run \
  --rm \
  --user "$(id -u):$(id -g)" \
  --volume "$PWD:/work" \
  "$ACTIVITY_RELAY_IMAGE" \
  generate-key \
  --output /work/actor.pem

Back up actor.pem. Replacing it changes the relay's cryptographic identity.

Before starting Compose, verify that both required bind mounts are regular files:

test -f actor.pem
test -f config.yml
contrib/docker/compose-preflight.sh "$PWD"

Do not run docker compose up with a missing actor.pem. Older Compose configurations may create a directory named actor.pem, which the relay cannot read as a private-key file. The included Compose file disables that automatic directory creation and fails early when either required file is absent.

Validate the resolved configuration and start the relay:

docker compose config
docker compose up -d

The Compose deployment pulls the image selected by ACTIVITY_RELAY_IMAGE, runs Redis, two workers, and the API server, and publishes the API on 127.0.0.1:8080 by default for a host reverse proxy. Change RELAY_PUBLISH_ADDRESS or RELAY_HTTP_PORT in .env when needed.

On Linux hosts running Redis in Docker, enable memory overcommit to avoid background-save failures under memory pressure:

sudo sysctl -w vm.overcommit_memory=1

printf 'vm.overcommit_memory=1
' |
  sudo tee /etc/sysctl.d/99-activity-relay-redis.conf

Inspect the deployment:

docker compose ps
docker compose logs --tail=100 server worker redis

curl --fail --silent --show-error \
  http://127.0.0.1:8080/status.json |
python3 -m json.tool

To build from the current checkout instead of using a published image:

docker compose \
  -f compose.yml \
  -f compose.build.yml \
  up -d --build

Verify an image:

docker run \
  --rm \
  ghcr.io/thystra/activity-relay:3.0.1 \
  --version

The image uses /usr/bin/relay as its entrypoint.

Native Debian/Ubuntu package

Tagged maintained-fork releases attach the canonical Ubuntu 24.04 amd64 package, SHA256SUMS, and release evidence to the authoritative Forgejo release. GitHub remains a downstream validation mirror and does not publish independent release assets.

Install a downloaded package:

sudo apt install ./activity-relay_VERSION_amd64.deb

The package:

  • generates /etc/activity-relay/actor.pem without replacing an existing key;
  • installs an example configuration but does not create an active config.yml;
  • installs inactive-by-default server, worker, Redis, and resource-guard units;
  • does not enable a web-server configuration;
  • preserves actor identity, local configuration, website content, and Redis data during upgrades and removal.

Continue with:

/usr/share/doc/activity-relay/README.Debian

Source build

For a tagged stable build:

VERSION=3.0.1

git checkout "v${VERSION}"
mkdir -p build

go build \
  -trimpath \
  -ldflags="-s -w -X main.version=${VERSION}" \
  -o build/relay \
  .

For a development build:

mkdir -p build

go build \
  -trimpath \
  -ldflags="-X main.version=$(git describe --tags --always --dirty | sed 's/^v//')" \
  -o build/relay \
  .

Running the relay

API server:

relay --config /path/to/config.yml server

Worker:

relay --config /path/to/config.yml worker

Management CLI:

relay --config /path/to/config.yml control

Manual directory lifecycle commands:

relay --config /path/to/config.yml directory status
relay --config /path/to/config.yml directory status https://directory.example.org
relay --config /path/to/config.yml directory register https://directory.example.org
relay --config /path/to/config.yml directory heartbeat https://directory.example.org
relay --config /path/to/config.yml directory sync https://directory.example.org
relay --config /path/to/config.yml directory unregister https://directory.example.org

Version:

relay --version

Configuration

A minimal container-oriented YAML configuration:

ACTOR_PEM: /var/lib/relay/actor.pem
REDIS_URL: redis://redis:6379

RELAY_BIND: 0.0.0.0:8080
# OBSERVABILITY_BIND: 0.0.0.0:9090
RELAY_DOMAIN: relay.example.org
RELAY_SERVICENAME: Community ActivityPub Relay
JOB_CONCURRENCY: 10

MAX_ACTIVITY_BYTES: 1048576
MAX_FANOUT_TARGETS: 5000
MAX_QUEUE_JOBS: 100000

# Outbound authorized-fetch and delivery signing. Omitted defaults to dual.
OUTBOUND_SIGNATURE_PROFILE: dual

# Fresh-install default. Existing installations can retain the
# pre-3.0 behavior with public_and_unlisted.
PUBLIC_ADDRESS_DISTRIBUTION_POLICY: explicit_public_only

# Optional scheduling is false by default and runs only in the API server.
# Example public Directory (disabled until explicitly enabled):
#   https://directory.argentwolf.org
# Maintained Directory index:
#   https://github.com/thystra/Activity-Relay/blob/master/docs/DIRECTORY-INDEX.md
DIRECTORY_SCHEDULER_ENABLED: false
# DIRECTORIES:
#   - origin: https://directory.argentwolf.org
#     enabled: false
#   - origin: https://directory.example.org
#     enabled: false
# Optional public descriptive metadata sent only to v2-capable Directories.
# DIRECTORY_PROFILE:
#   # Registration status: open, restricted, or closed.
#   participation_mode: open
#   availability: public
#   relay_type: general
#   languages: [en]
#   countries: [US]
#   regions: []
#   topics: [general]
#   contact_fediverse: "@relay@example.social"
#   contact_email: relay@example.org
#   contact_url: https://relay.example.org/contact
#   participation_url: https://relay.example.org/join
#   notes: Public community relay
# Optional presentation-only support methods for the bundled landing site.
# SUPPORT:
#   - title: "Liberapay"
#     url: "https://liberapay.com/example/"
#   - title: "Bitcoin"
#     value: "bc1qexample"

# RELAY_SUMMARY: |
# Optional public relay branding. These are interoperability recommendations,
# not protocol limits; clients may crop or rescale the supplied images.
# RELAY_ICON: square logo/avatar; 512x512 px recommended (128x128 minimum).
# Keep important icon content centered and use a public HTTPS URL.
# RELAY_ICON: https://relay.example.org/assets/relay-icon.png
# RELAY_IMAGE: wide header/banner; 1500x500 px (3:1) recommended.
# Keep important banner content away from the outer edges.
# RELAY_IMAGE: https://relay.example.org/assets/relay-banner.webp

Use 127.0.0.1:8080 instead when the relay and reverse proxy run directly on the same host.

PUBLIC_ADDRESS_DISTRIBUTION_POLICY accepts explicit_public_only or public_and_unlisted. explicit_public_only distributes activities only when the ActivityStreams Public collection appears in the primary to audience. A Public address appearing only in cc is acknowledged and publisher-accounted but is not distributed through public fan-out. public_and_unlisted preserves the pre-3.0 behavior by including Public from either to or cc; it does not make followers-only or direct activities eligible. The fresh configuration example explicitly selects explicit_public_only. An omitted value falls back to public_and_unlisted for compatibility, but upgrade configurations should set that value explicitly when retaining the old behavior.

OUTBOUND_SIGNATURE_PROFILE accepts legacy, rfc9421, or dual. Empty or omitted configuration selects dual, the 3.0 compatibility default. dual uses one concrete wire profile per operation: unknown authenticated GETs begin with RFC 9421, while unknown delivery POSTs begin with legacy and never switch signature grammar across retries. Operators may set legacy or rfc9421 explicitly when they need a fixed profile. The same policy is used by the API server for signed remote GETs and by every worker for delivery POSTs.

DIRECTORIES is an optional list of at most eight canonical HTTPS origins. Origins cannot contain credentials, paths, queries, fragments, or the explicit default port. Each entry has its own enabled boolean; omission means there are no directory endpoints, and an omitted boolean is false. Register, heartbeat, and sync require the selected entry to be enabled. Setting DIRECTORY_SCHEDULER_ENABLED: true in a regular YAML file enables startup reconciliation and daily heartbeats only in the API process; workers never run the scheduler. ActivityPub signing is unchanged. See docs/DIRECTORY-CLIENT.md.

A public Directory operators may choose to register with is https://directory.argentwolf.org. Other known Directory servers are listed in docs/DIRECTORY-INDEX.md, with the GitHub mirror at https://github.com/thystra/Activity-Relay/blob/master/docs/DIRECTORY-INDEX.md providing a convenient live view. The index is informational rather than a trust or endorsement list, and every Directory remains opt-in.

The scheduler persists bounded Redis state, coordinates multiple API processes with renewable per-directory leases, and schedules successful heartbeats after 24 hours plus up to two hours of stable jitter. Automatic retry starts at 30 seconds, caps its local backoff at 15 minutes, and allows validated remote Retry-After guidance to lengthen the effective delay up to 24 hours. State writes atomically verify the current lease token, so a former owner cannot overwrite a successor. Directory failures never block relay startup or delivery. Environment-only scheduling is unsupported because durable unregister suppression must be read from the same regular YAML file.

directory status without an origin lists local entry state. With an origin it retrieves that Directory's strict public status document. Status schemas 2 and 3 mean Protocol v1 only; schema 4 advertises supported lifecycle protocol versions. When Protocol v2 is advertised, registration includes DIRECTORY_PROFILE and may include the relay's bounded receiving-site count; v2 heartbeats may refresh that count. Otherwise lifecycle operations use Protocol v1. directory sync explicitly reconciles registration and the current descriptive profile. DIRECTORY_PROFILE.participation_mode accepts only open, restricted, or closed. Invalid optional profile fields are warned about and omitted rather than preventing the relay from serving ActivityPub.

When the bundled static landing site is rebuilt with --relay-config, it reads the same local DIRECTORY_PROFILE and renders a Relay information box near the top of the home page. Registration/contact information is shown first; relay type/topics and any declared language/country/region focus are grouped separately below. Empty language/country/region lists mean that no focus is declared and the location-focus subsection is omitted. The profile remains descriptive and does not change delivery or follow policy.

An optional top-level SUPPORT list may also provide up to eight public support methods for the bundled site. Each entry contains title and exactly one absolute HTTPS url or plain-text value. The generated site escapes all text, uses no remote embeds/scripts/pixels for support, and renders the block collapsed by default. SUPPORT is ignored by the relay runtime and is never sent to a Directory.

For a regular file-backed configuration, directory unregister first atomically changes the selected entry to enabled: false, preserves ownership, mode, unrelated YAML, and comments, and writes a recoverable sibling backup ending in .activity-relay.bak. Only after that durable local change does it send the signed unregister request. A remote failure returns nonzero and leaves the entry disabled. --remove removes the disabled entry only after remote success while retaining the pre-disable backup.

File-backed unregister acquires the same lease used by the scheduler whenever REDIS_URL remains configured, including after the scheduler gate is turned off. It validates persisted state under that lease, durably disables the entry, and uses a lease-token-fenced suppression write before the remote request. Gate disablement and entry removal are durable suppression rather than scheduler failures. A failed remote unregister is shown by local directory status as unregister-pending; it cannot be re-registered on restart. SIGINT and SIGTERM cancel in-flight scheduler work and gracefully stop the API listeners.

For the bundled Compose deployment, config.yml is a single-file read-only bind. After an atomic host replacement, a running container may still see the old inode. Before containerized unregister, persist enabled: false on the host and force-recreate the API/server so its scheduler sees the disabled file, but keep that API online while the Directory resolves the relay actor/key and authenticates the unregister. After re-enabling, recreate the API/server again so automatic registration sees the new inode. Recreate workers after either replacement as bind-mount housekeeping. File-backed unregister also needs writable access to the configuration directory for its sibling backup/temp files and atomic rename; do not make actor.pem writable. See docs/DIRECTORY-CLIENT.md for the full sequence.

When the configuration file is absent, manual commands may read ACTOR_PEM, RELAY_DOMAIN, a YAML or JSON DIRECTORIES sequence, and an optional YAML DIRECTORY_PROFILE mapping from the environment. Because Activity-Relay cannot mutate an external configuration source, environment-only unregister refuses to proceed unless --acknowledge-external-disable is supplied. Disable that external entry before restarting the relay.

Configuration can be checked before restart with relay -t -c /path/to/config.yml (or relay --test-config). Optional Directory-profile problems are reported with line-aware warnings and omitted from publication; add --strict to make those warnings fail the configuration test. Structural/runtime configuration errors remain fatal.

dual uses expiring Redis capability evidence scoped separately to fetches and deliveries. An unknown GET tries RFC 9421 and may make one legacy fallback after either a 401 or 403 response containing an explicit WWW-Authenticate: Signature challenge, or the bounded HTTP 400 compatibility signal. The 400 path is limited to unknown idempotent fetches, and a short-lived legacy preference is recorded only when the legacy retry succeeds. Other generic HTTP failures, response body text, DNS failures, and timeouts do not trigger fallback. An unknown delivery uses legacy. Every newly queued delivery records one concrete wire profile, which remains unchanged across delayed retries; a POST is never resent under a different signature profile.

RELAY_ICON and RELAY_IMAGE are optional public metadata URLs. The suggested dimensions are compatibility-oriented recommendations rather than enforced limits. A square icon is least likely to be distorted by clients, while a 3:1 banner matches common profile-header layouts. Clients may still resize or center-crop either image, so keep identifying content away from the edges.

When no configuration file exists, these runtime values may be supplied as environment variables:

ACTOR_PEM
REDIS_URL
RELAY_BIND
OBSERVABILITY_BIND
RELAY_DOMAIN
RELAY_SERVICENAME
JOB_CONCURRENCY
MAX_ACTIVITY_BYTES
MAX_FANOUT_TARGETS
MAX_QUEUE_JOBS
OUTBOUND_SIGNATURE_PROFILE
PUBLIC_ADDRESS_DISTRIBUTION_POLICY
DIRECTORIES
DIRECTORY_PROFILE
RELAY_SUMMARY
RELAY_ICON
RELAY_IMAGE

Operational storage, cache, mail, and daily-summary settings are documented in contrib/ops/README.md.

Observability listener

Observability is disabled when OBSERVABILITY_BIND is empty or unset. When it is configured for the server command, Activity-Relay starts a separate HTTP listener exposing only:

GET /metrics
GET /-/healthy
GET /-/ready

/-/healthy is process-only liveness. /-/ready performs a bounded Redis ping and returns 503 Service Unavailable when the required runtime store is not available. /metrics uses a private Prometheus registry and initially reports Go and process metrics, build information, Redis readiness, and public API HTTP request counts and durations. HTTP labels use a fixed route set, common methods, and numeric response codes; raw paths, query strings, domains, and error text are never labels.

For a native installation, prefer a loopback binding such as:

OBSERVABILITY_BIND: 127.0.0.1:9090

For containers, bind to the private container network only and do not publish the observability port on an untrusted interface. The bundled Compose file does not publish an observability port. Do not forward these routes through the public ActivityPub reverse proxy.

Example local checks:

curl --fail http://127.0.0.1:9090/-/healthy
curl --fail http://127.0.0.1:9090/-/ready
curl --fail http://127.0.0.1:9090/metrics

The worker command does not open this listener. When observability is enabled in the shared configuration, API and worker processes write bounded operational counters to Redis and the API process exports them through /metrics. The operational surface includes activity outcomes, queue admission and depth, fan-out targets, delivery results, directory scheduler outcomes, Redis-operation failures, current receiver and publisher counts, and aggregate receiver-health states.

Metric labels are closed enums. Domains, actor IDs, inbox URLs, activity IDs, raw paths, query strings, response bodies, and error text are never labels. Complete Redis outages are represented by readiness and collection-success metrics because a failed Redis server cannot also persist its own failure counter.

Operational summary scheduling

The native resource guard can send one or more reports per local day:

DAILY_SUMMARY_EMAIL: true
DAILY_SUMMARY_TIMES:
  - "08:00"
  - "14:30"
MAIL_TIMEOUT_SECONDS: 60

The values are server-local, zero-padded 24-hour HH:MM times. The guard timer runs approximately every five minutes, so mail is processed on the first timer run at or after the configured time and may not arrive at the exact minute.

Each time is an independent daily slot. Changing or adding a time allows that new slot to send even when another report was already sent that day. If multiple slots were missed during downtime, only the most recent due slot is sent. Its catch-up email lists skipped slots and the commands used to inspect or reset them.

sudo activity-relay-resource-guard --show-summary-state
sudo activity-relay-resource-guard --preview-summary
sudo activity-relay-resource-guard --send-summary-now
sudo activity-relay-resource-guard   --reset-summary-slot "14:30"   --force

Successful report bodies are archived below /var/lib/activity-relay-guard/summaries/. Skipped slots are recorded in /var/lib/activity-relay-guard/summary-slots.json, but no historical report body exists for a skipped time because no snapshot was captured. Resetting a slot and running the guard sends current state rather than reconstructing past state.

--send-summary-now does not consume a scheduled slot. Preview and --no-mail runs do not consume slots. The deprecated DAILY_SUMMARY_HOUR remains compatible as a single HH:00 schedule.

The container image includes the administrative CLI, but Compose does not schedule it or provide an MTA. Persist the guard state directory and provide a mail transport when using it from a host scheduler or sidecar. Full native and container notes are in contrib/ops/README.md.

Federation endpoints

Mastodon, Misskey, and compatible software subscribe to:

https://relay.example.org/inbox

Pleroma, Akkoma, Friendica, and compatible software follow:

https://relay.example.org/actor

A public reverse proxy should forward these relay routes:

/inbox
/actor
/actor/outbox
/actor/followers
/actor/following
/status.json
/.well-known/nodeinfo
/.well-known/webfinger
/nodeinfo/2.1

The public proxy should not forward /metrics, /-/healthy, or /-/ready; those routes belong only to the separately bound observability listener.

The actor advertises inbox, outbox, followers, following, and endpoints.sharedInbox. It is published as an ActivityStreams Application named relay at /actor so current Friendica relay discovery recognizes it. This classification does not change the actor ID, endpoints, collections, public-key ID, or underlying actor key. Public GET requests to the collection endpoints return privacy-filtered empty OrderedCollection documents. The relay does not expose subscriber identities or historical relayed activities, and it does not implement ActivityPub client-to-server POSTs to the outbox.

The relay accepts standards-style server actors of type Application or Service following the relay actor, regardless of whether their actor URL ends in /actor, /relay, /friendica, or another implementation-defined path. Legacy /relay and /friendica actors with incomplete type metadata remain supported.

Outbound authorized fetches and deliveries use OUTBOUND_SIGNATURE_PROFILE. The 3.0 default is destination-aware dual: it preserves legacy delivery for unknown peers while using or learning RFC 9421 capability independently for fetch and delivery scopes. Explicit legacy and rfc9421 remain available as fixed profiles. RFC 9421 uses RFC 9530 Content-Digest without changing the actor key or key ID. All wire profiles sign the same authority transmitted on the wire, including a non-default port. Bounded non-success response text is included in worker errors to make remote signature-verification failures diagnosable without logging unbounded response bodies.

Public Announce interoperability

Some ActivityPub servers, including NodeBB category actors, publish locally created content as a public Announce containing an embedded Article or Note. The relay replaces that transport wrapper with one relay-signed Announce referencing the embedded object ID, then sends the same authenticated wrapper to both traditional relay subscribers and follower-style subscribers. This keeps the HTTP signer and JSON activity actor aligned for strict receivers such as Mastodon while preserving the original NodeBB object and author when the receiver fetches the referenced object.

Public Announce activities whose object is only a URL, or whose embedded object belongs to another domain, remain publisher-accounting events and are not fanned out. This preserves relay-to-relay loop protection and avoids amplifying ordinary boosts.

NodeBB 4.14.x, including 4.14.5 testing, exposed a receiving-side secure-mode limitation: its application-context canonical-object fetch was unsigned, so it could return HTTP 424 after accepting a valid relay delivery when the remote object required a signed GET. NodeBB upstream changed that path in commit 8e61543b0ae19fd741bd4175d478aab6c79982ca; a stock NodeBB 4.15.1 retest passed during the 3.0 RC acceptance cycle. A separate forced-RFC-9421 Announce delivery currently receives HTTP 400 from NodeBB 4.15.1 and is tracked upstream as NodeBB issue #14732. Activity-Relay's normal dual policy does not infer delivery capability from fetch-only evidence. See docs/INTEROPERABILITY.md.

Inbound request failures are logged with the HTTP method, path, remote address, user agent, and the bounded verification or decoding error. Request bodies, signatures, and key material are not logged.

See docs/INTEROPERABILITY.md for the validated server matrix, NodeBB-specific behavior, and troubleshooting guidance.

Relay public-key encoding

The relay actor publishes its RSA public key as X.509 SubjectPublicKeyInfo PEM, using -----BEGIN PUBLIC KEY-----. This is the common interoperable form used by ActivityPub implementations for publicKeyPem. Inbound signature verification accepts both SubjectPublicKeyInfo and legacy PKCS#1 -----BEGIN RSA PUBLIC KEY----- actor keys.

Changing the public encoding does not rotate actor.pem, change the relay actor ID, or change the #main-key key ID. Existing subscribers continue to reference the same cryptographic identity.

Public status endpoint

The API server exposes:

GET /status.json

Schema version 5 reports relay identity and policy, endpoints, software version, the effective public_address_distribution_policy, its human-readable label, and three related domain views:

  • connected_instances: the deduplicated set of domains that receive relay traffic, publish accepted activities, or do both;
  • receiving_instances: the narrower set that receives fan-out, including delivery-health timestamps and counters for currently registered receivers;
  • publishers: observed sending domains, last-seen metadata, activity count, and whether each domain also receives the relay.

Example:

curl --fail --silent --show-error \
  http://127.0.0.1:8080/status.json |
python3 -m json.tool

The endpoint does not expose Redis keys, queue internals, blocked-domain lists, actor IDs, inbox URLs, or private configuration.

Open publisher ingestion still enforces signature validation, actor/key-host matching, blocked and limited-domain policy, and person-only policy.

Optional public website

The bundled frontend is optional and has no effect on relay operation. The default generated footer reads the configured status endpoint at runtime and discloses the effective public-address distribution policy on every page. Operators may:

  1. use the bundled generated site;
  2. serve a custom site that reads /status.json;
  3. redirect the root page elsewhere;
  4. return 404 for all non-relay paths.

Disable the frontend with Nginx

Keep the exact relay endpoint locations, and use:

location / {
    return 404;
}

Redirect the root page

location = / {
    return 302 https://example.org/about-this-relay;
}

location / {
    return 404;
}

Custom status page

A same-origin custom page can request:

fetch("/status.json")
  .then((response) => response.json())
  .then((status) => {
    document.querySelector("#server-count").textContent =
      status.connected_instances.count;
  });

When using the sample Content Security Policy, place JavaScript in a separate same-origin file. A ready-to-copy example is included under:

contrib/web/examples/

Build the bundled site on a native installation

The Debian package installs website sources in /usr/share/activity-relay/web. Keep the operator-owned editable copy in /etc/activity-relay-web and generated output in /var/www/activity-relay/public.

After editing operator-owned settings or overrides, rebuild with the current package-managed source:

sudo activity-relay-rebuild-site

A different web root can be selected explicitly:

sudo activity-relay-rebuild-site \
  --output /srv/www/relay.example.org

The package-managed command is authoritative for Debian installations. For a source checkout or fully user-owned directories, use contrib/web/activity-relay-rebuild-site with explicit paths.

Apache frontend choices

A complete Apache 2.4 example is included at:

contrib/apache/activity-relay.conf.example

The file lists required modules and includes bundled-site, no-frontend, redirected-root, and custom-document-root guidance. When changing the output directory, change both DocumentRoot and the corresponding <Directory> path.

Caddy frontend choices

A complete optional Caddy 2 example and operator notes are included at:

contrib/caddy/Caddyfile.example
contrib/caddy/README.md

The example uses Caddy's automatic HTTPS, serves the bundled static site, and proxies only the required relay endpoints to 127.0.0.1:8080. It is an operator example only; Activity-Relay does not install, enable, or depend on Caddy.

Build the bundled site from a container image

Published images contain website sources at:

/usr/share/activity-relay/web

The 3.0.0 image includes Python 3 for resource-guard tooling and website generation. To customize the website outside the running relay, extract the sources:

export ACTIVITY_RELAY_IMAGE='ghcr.io/thystra/activity-relay:3.0.0'

mkdir -p \
  activity-relay-web \
  activity-relay-public

container_id="$(docker create "$ACTIVITY_RELAY_IMAGE")"

docker cp \
  "$container_id:/usr/share/activity-relay/web/." \
  ./activity-relay-web/

docker rm "$container_id"

cp -n \
  ./activity-relay-web/site.json.example \
  ./activity-relay-web/site.json

Customize the source and generate it with the same release image:

docker run \
  --rm \
  --user "$(id -u):$(id -g)" \
  --entrypoint python3 \
  --volume "$PWD/activity-relay-web:/site:ro" \
  --volume "$PWD/activity-relay-public:/output" \
  "$ACTIVITY_RELAY_IMAGE" \
  /site/build-site.py \
    --source /site \
    --config /site/site.json \
    --output /output

Serve activity-relay-public with the host reverse proxy or a separate web container.

Full website, Nginx, Apache, Caddy, customization, and replacement instructions are in contrib/web/README.md.

Testing

Do not run tests against production Redis. One disposable Docker-based test run is:

docker rm -f activity-relay-test-redis \
  >/dev/null 2>&1 || true

docker run \
  --detach \
  --rm \
  --name activity-relay-test-redis \
  --publish 127.0.0.1:6381:6379 \
  redis:7-alpine

until docker exec activity-relay-test-redis redis-cli ping |
  grep -qx PONG
do
  sleep 1
done

REDIS_URL='redis://127.0.0.1:6381' \
  go test -count=1 -p 1 ./...
REDIS_URL='redis://127.0.0.1:6381' \
  go test -race -count=1 -p 1 ./...

go vet ./...

python3 -m unittest discover \
  -s contrib/web \
  -p 'test_*.py'

python3 -m unittest discover \
  -s contrib/ops \
  -p 'test_*.py'

docker rm -f activity-relay-test-redis

Also run:

git diff --check

Contributor and coding-agent expectations are documented in AGENTS.md. See ARCHITECTURE.md for component and data-flow design, TODO.md for the maintained roadmap and release gates, docs/INTEGRATION-TESTING.md for the container and native-package validation matrix, and docs/SECURITY.md for the HTTP-signature compatibility profile and current RFC 9421/RFC 9530 negotiation behavior.

Releases

Maintainer release steps are documented in docs/RELEASING.md. Versioned release notes are kept under docs/releases/. The current stable release is v3.0.0. Its accepted release-candidate history is retained in v3.0.0-rc2 and v3.0.0-rc1. Earlier stable releases v2.5.1, v2.5.0, and v2.4.0 remain available for historical reference. Historical RC notes remain under the same directory.

Upstream and attribution

This project is derived from:

Original authorship, commit history, license notices, and attribution are retained. Generally useful fixes may be proposed upstream; the maintained fork also publishes its own tested release line. Maintainers should follow docs/UPSTREAM.md when reviewing or porting future upstream changes.

License

GNU Affero General Public License version 3. See LICENCE.

Upgrade-safe website customization

The package separates current program files from operator-owned content:

/usr/share/activity-relay/web/
    Current package-managed builder, templates, JavaScript, CSS, and defaults.

/etc/activity-relay/config.yml
    Relay behavior and shared public metadata:
    RELAY_ICON
    RELAY_IMAGE
    FEDIVERSE_OPERATOR_ID
    FEDIVERSE_OPERATOR_URL

/etc/activity-relay-web/site.json
    Website name, tagline, email/contact URL, source URL, language, and optional
    website-specific logo_url or banner_url overrides.

/etc/activity-relay-web/content/
    Optional operator replacements for:
    home.html
    about.html
    rules.html
    privacy.html
    footer.html

/etc/activity-relay-web/custom-assets/
    Optional replacement or additional public assets.

/var/www/activity-relay/public/
    Generated output. Do not edit it directly.

To customize rules without copying the whole website source:

sudo install -d -o root -g root -m 0755 \
  /etc/activity-relay-web/content

sudo cp \
  /usr/share/activity-relay/web/content/rules.html \
  /etc/activity-relay-web/content/rules.html

sudoedit /etc/activity-relay-web/content/rules.html

Use the same pattern for home.html, about.html, privacy.html, or footer.html. Files not present in the override directory come from the current package, so package fixes remain effective.

Set shared branding and the public operator handle in /etc/activity-relay/config.yml:

RELAY_ICON: "https://relay.example.org/images/relay-icon.png"
RELAY_IMAGE: "https://relay.example.org/images/relay-banner.png"
FEDIVERSE_OPERATOR_ID: "@operator@social.example"
# Optional because profile URL patterns differ among fediverse applications:
FEDIVERSE_OPERATOR_URL: "https://social.example/@operator"

The canonical configuration names use underscores. The builder also accepts the legacy hyphenated aliases FEDIVERSE-OPERATOR-ID and FEDIVERSE-OPERATOR-URL. The website-specific logo_url and banner_url values in site.json take precedence when non-empty.

Rebuild with the current package source:

sudo activity-relay-rebuild-site

An older regular file at /etc/activity-relay-web/rebuild-site.sh may be preserved during upgrades. The package-managed command above is authoritative.