- Go 85.3%
- Python 10.5%
- Shell 1.7%
- JavaScript 0.9%
- HTML 0.8%
- Other 0.8%
| .forgejo/workflows | ||
| .github | ||
| api | ||
| contrib | ||
| control | ||
| debian | ||
| deliver | ||
| docs | ||
| internal | ||
| misc | ||
| models | ||
| scripts/release | ||
| testdata | ||
| .dockerignore | ||
| .env.example | ||
| .gitignore | ||
| AGENTS.md | ||
| ARCHITECTURE.md | ||
| CHANGELOG.md | ||
| compose.build.yml | ||
| compose.yml | ||
| config.yml.example | ||
| Dockerfile | ||
| go.mod | ||
| go.sum | ||
| LICENCE | ||
| main.go | ||
| main_signal_test.go | ||
| readme.md | ||
| TODO.md | ||
Activity Relay Server
A maintained and deployable ActivityPub relay written in Go
Note
This repository is a maintained fork of
yukimochi/Activity-Relay, based on upstream releasev2.0.10.The authoritative source repository is
forgejo.argentwolf.org/alan/activity-relay.github.com/thystra/Activity-Relayis a downstream public mirror with independent validation. The public Go module path remainsgithub.com/thystra/Activity-Relay.
Highlights
Compared with the upstream baseline, this fork includes:
- Relay-signed actor and canonical-object
GETrequests for Mastodon authorized-fetch and secure-mode interoperability. - RFC 9421 HTTP Message Signatures and RFC 9530
Content-Digestwith strict inbound verification and explicitlegacy,rfc9421, and destination-awaredualoutbound modes; 3.0 usesdualas 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
Applicationrelay 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
ApplicationorServiceactors, while preserving/relayand/friendicalegacy 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.jsonschema version 5 with:public_address_distribution_policyand 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/amd64andlinux/arm64container releases on GHCR. - Native Ubuntu 24.04
amd64Debian 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.pemwithout 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:
- use the bundled generated site;
- serve a custom site that reads
/status.json; - redirect the root page elsewhere;
- return
404for 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:
yukimochi/Activity-Relay- upstream baseline
v2.0.10
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.