No description
  • Go 96.6%
  • Shell 1.5%
  • HTML 0.9%
  • CSS 0.7%
  • Python 0.2%
  • Other 0.1%
Find a file
Alan Johnson 0c569707b9
Some checks failed
Build / Container (push) Successful in 1m14s
Debian Package / Debian package validation (push) Has been cancelled
Test / Go 1.26.5 (push) Has been cancelled
Test / Go 1.26.0 (push) Has been cancelled
Test / Go 1.27rc2 (forward) (push) Has been cancelled
Add relay list in CSV form
2026-10-05 00:25:21 -04:00
.forgejo/workflows canonical artifact wrapper metadata 2026-10-03 15:20:45 -04:00
.github Release prep for 1.2.0 2026-10-03 05:21:35 -04:00
cmd/activity-relay-directory hardening and test issues 2026-10-04 22:27:01 -04:00
contrib Improve public Directory UX and RC acceptance 2026-08-12 20:00:56 -04:00
debian resolve lintian error 2026-10-04 22:41:54 -04:00
docs Update versioning for RC3 2026-10-04 23:09:56 -04:00
internal hardening and test issues 2026-10-04 22:27:01 -04:00
scripts hardening and test issues 2026-10-04 22:27:01 -04:00
testdata hardening and test issues 2026-10-04 22:27:01 -04:00
.dockerignore Initialize Activity-Relay Directory scaffold 2026-08-04 13:02:43 -04:00
.env.example Add background relay reachability maintenance 2026-09-14 12:32:19 -04:00
.gitignore Wire SQLite into runtime readiness 2026-08-04 20:18:17 -04:00
AGENTS.md hardening and test issues 2026-10-04 22:27:01 -04:00
ARCHITECTURE.md Add profile persistence 2026-10-03 16:04:01 -04:00
ard-relay-list.csv Add relay list in CSV form 2026-10-05 00:25:21 -04:00
CHANGELOG.md hardening and test issues 2026-10-04 22:27:01 -04:00
compose.yml Add background relay reachability maintenance 2026-09-14 12:32:19 -04:00
config.yml.example Address RC1 findings 2026-10-04 13:30:39 -04:00
Dockerfile Prepare v0.1.0-rc3 release candidate 2026-08-13 23:50:49 -04:00
go.mod Prepare v0.1.0-rc3 release candidate 2026-08-13 23:50:49 -04:00
go.sum Prepare v0.1.0-rc3 release candidate 2026-08-13 23:50:49 -04:00
LICENCE Initialize Activity-Relay Directory scaffold 2026-08-04 13:02:43 -04:00
README.md hardening and test issues 2026-10-04 22:27:01 -04:00
SECURITY.md Add 1.1 public directory projection 2026-09-14 17:49:48 -04:00
TODO.md hardening and test issues 2026-10-04 22:27:01 -04:00

Activity-Relay Directory

Activity-Relay Directory is a self-hosted directory and health service for public ActivityPub relays. It can learn about relays from authenticated lifecycle heartbeats or local operator discovery, check whether those relays are reachable, and publish a human-readable directory, JSON APIs, and plain-text host lists.

The service tracks relay instances only. It is not intended to collect the identities of servers, followers, users, or communities connected to a relay.

What it provides

  • a public relay directory with four clear availability tiers;
  • optional authenticated register, heartbeat, and unregister support for relays;
  • local commands to add or import relays that do not send directory heartbeats;
  • background reachability checks and retry of unavailable relays;
  • JSON APIs at /v1/relays and /v2/relays, including bounded self-reported receiving-site telemetry when available;
  • plain-text relay lists suitable for reuse in discovery imports;
  • local moderation, pruning, retention, storage, and audit commands; and
  • SQLite persistence designed for one active directory process on one host.

All network-facing features that can change state are disabled by default. Public listing, relay lifecycle participation, background reachability checks, and automatic pruning are enabled independently.

Directory tiers

The public directory groups relays by operational status rather than popularity. Within each tier, relays that self-report registration as open are shown first, then restricted, closed, and unreported status, with normalized hostname as the stable tie-breaker. Registration status is descriptive and never changes a relay's operational tier. Heartbeat frequency and check recency do not improve a relay's tier.

Tier Meaning
Tier 1 — Heartbeat + Online The relay is sending a current directory heartbeat and was reachable at the latest check.
Tier 2 — Online, no current heartbeat The relay is known to the directory and was reachable at the latest check, but is not currently sending directory heartbeats.
Tier 3 — Offline / Unreachable The relay could not be reached at the latest check but has been seen online within the last 30 days.
Tier 4 — Graveyard The relay has not been seen online for at least 30 days. It remains listed for historical reference and is still checked periodically in case it returns.

A recovered relay moves back to Tier 1 or Tier 2 automatically according to whether it has a current heartbeat.

Quick start from source

For a local development instance:

mkdir -p data
chmod 0700 data

export DIRECTORY_PUBLIC_BASE_URL=http://127.0.0.1:8080
export DIRECTORY_DATABASE_PATH="$PWD/data/directory.sqlite"
export DIRECTORY_PUBLIC_LISTING_ENABLED=true

go run ./cmd/activity-relay-directory

Then open http://127.0.0.1:8080/.

The local HTTP base URL above is for loopback development only. A public installation should use HTTPS and a reverse proxy. See docs/REVERSE-PROXY.md.

Docker Compose

The included Compose file keeps the same conservative defaults as the service. Copy the example environment file, set the public URL and the features you want, then start the service:

cp .env.example .env

# Edit .env, then:
docker compose up --build

By default the published port is bound to 127.0.0.1, lifecycle participation is disabled, and the public directory is disabled.

Debian/Ubuntu package

The Debian package installs the service account, state directory, defaults file, and hardened systemd unit, but deliberately does not enable or start the service automatically.

After installing the package, review:

/etc/default/activity-relay-directory

Then enable the service when ready:

sudo systemctl enable --now activity-relay-directory

Package upgrades reload systemd's unit definitions but do not restart an already-running Directory. Review the upgrade and restart the service manually when you are ready to load the new binary and unit settings.

Package removal preserves the database and service account. Package purge is destructive. See debian/README.Debian before upgrade, downgrade, removal, or purge.

Add relays manually

A relay may be added from its base URL, /actor, or /inbox URL. Activity-Relay Directory resolves and validates the relay actor before storing it as an active discovery.

activity-relay-directory admin discovery add \
  --url https://relay.example/ \
  --operator operator-id \
  --reason public_relay

For a list of relays:

activity-relay-directory admin discovery import \
  --file ./relay-candidates.txt \
  --operator operator-id \
  --reason public_list \
  --source-label curated_list

Bare hostnames are accepted and treated as HTTPS candidates. Explicit HTTP URLs remain rejected. A file may contain blank lines and # comments.

For spreadsheet-friendly descriptive relay profiles, use CSV input explicitly:

activity-relay-directory admin discovery import \
  --file ./relay-candidates.csv \
  --input-format csv \
  --operator operator-id \
  --reason public_list \
  --source-label curated_list

CSV import keeps the same actor-validation and confirmation boundary as normal discovery. The line-oriented format remains the default. See docs/RELAY-PROFILES.md for the reviewed columns, profile precedence, private source_url provenance, and spreadsheet-safety rules.

If you also want to remember relays that are currently unreachable or return an incompatible actor document, add --add-dead-relays:

activity-relay-directory admin discovery import \
  --file ./relay-candidates.txt \
  --operator operator-id \
  --reason public_list \
  --source-label curated_list \
  --add-dead-relays

Those unresolved candidates remain private until a later check successfully validates the relay actor. Retry timing is 6 hours, 12 hours, 24 hours, 3 days, and then once a week.

See docs/DISCOVERY-REACHABILITY.md for the full discovery behavior.

Keep relay status current

Enable background reachability maintenance with:

DIRECTORY_REACHABILITY_ENABLED=true

Reachability checks are independent of authenticated heartbeats. A relay cannot move into the heartbeat tier merely because it responds to a network check, and a failed network check does not rewrite its heartbeat history.

Known relays that stay offline are eventually checked weekly. Relays that have not been seen online for 30 days remain in the database and move into the Graveyard tier rather than being silently deleted.

See docs/REACHABILITY.md.

Publish the directory

Enable public views with:

DIRECTORY_PUBLIC_LISTING_ENABLED=true

That exposes these read-only routes:

Route Purpose
/ Public facing relay directory
/v1/relays Version 1 compatibility API
/v2/relays Richer tiered relay API
/downloads/active.txt Tier 1 and Tier 2 hosts
/downloads/unavailable.txt Tier 3 and Tier 4 hosts
/downloads/all.txt All public relay hosts
/healthz Process health
/readyz Readiness, including storage availability
/v1/status Directory lifecycle/enrollment status

Enabling the public directory does not enable lifecycle registration or open enrollment. The human page summarizes known relays as online/offline and may show the aggregate number of unresolved candidates still pending verification; it does not disclose those candidate identities.

The public APIs and downloads intentionally omit local discovery provenance, operator reasons, moderation records, audit events, request signatures, and other private administrative data. See docs/PUBLIC-LISTING.md.

Export relay lists locally

You can generate the same tier-scoped relay lists without enabling public listing:

activity-relay-directory admin export --scope active --format hosts
activity-relay-directory admin export --scope unavailable --format hosts
activity-relay-directory admin export --scope all --format actors
activity-relay-directory admin export --scope all --format csv > relays.csv

Scopes are:

  • active: Tier 1 and Tier 2;
  • unavailable: Tier 3 and Tier 4; and
  • all: all four public tiers.

hosts writes one normalized host per line, including non-default HTTPS ports. actors writes normalized relay actor URLs. csv writes canonical relay actor identity plus the effective descriptive profile fields, with private provenance omitted and spreadsheet-formula cells reversibly neutralized. Host and CSV exports can be fed back into a later discovery import; CSV re-import requires --input-format csv.

Optional relay lifecycle heartbeats

Authenticated register, heartbeat, and unregister routes are controlled by:

DIRECTORY_LIFECYCLE_ENABLED=true

They are disabled by default. Enrollment is a separate policy and starts closed. A local operator can inspect or change it with:

activity-relay-directory admin enrollment status
activity-relay-directory admin enrollment open --operator operator-id
activity-relay-directory admin enrollment close --operator operator-id

Closing enrollment prevents previously unseen relays from registering; it does not remove or disable relays already known to the directory.

The lifecycle protocol is documented in docs/PROTOCOL.md. The companion relay implementation is thystra/Activity-Relay.

Local administration

Administrative commands are local-only; there is no public moderation API. Examples:

activity-relay-directory admin show \
  --actor https://relay.example/actor \
  --format json

activity-relay-directory admin suspend \
  --actor https://relay.example/actor \
  --moderator operator-id \
  --reason security_review

activity-relay-directory admin restore \
  --actor https://relay.example/actor \
  --moderator operator-id \
  --reason review_complete

activity-relay-directory admin audit \
  --actor https://relay.example/actor \
  --limit 50 \
  --format json

Other local maintenance commands include:

activity-relay-directory admin pruning dry-run --format json
activity-relay-directory admin retention dry-run --format json
activity-relay-directory admin storage status --format json
activity-relay-directory admin storage check

Hard retention is disabled by default. Automatic pruning is also disabled by default and is reversible rather than destructive. Review docs/MODERATION.md, docs/RETENTION.md, and docs/STORAGE-GROWTH.md before changing those policies.

Core configuration

The full configuration matrix is in docs/CONFIGURATION.md. The settings most operators will encounter first are:

Variable Default / requirement Purpose
DIRECTORY_PUBLIC_BASE_URL required Public directory URL; HTTPS for lifecycle use
DIRECTORY_DATABASE_PATH required Local SQLite database path
DIRECTORY_LISTEN_ADDRESS 127.0.0.1:8080 HTTP listener
DIRECTORY_PUBLIC_LISTING_ENABLED false Publish the human, JSON, and text directory views
DIRECTORY_LIFECYCLE_ENABLED false Enable authenticated register/heartbeat/unregister routes
DIRECTORY_REACHABILITY_ENABLED false Enable periodic actor/inbox reachability checks
DIRECTORY_SOFT_PRUNING_ENABLED false Enable automatic reversible pruning
DIRECTORY_INACTIVE_RETENTION_DAYS 0 Hard retention age; 0 keeps inactive records indefinitely

The retired DIRECTORY_REGISTRATION_ENABLED setting is rejected rather than accepted as an alias.

Optional operator contact

The human directory can show operator-owned contact links without exposing private administrator-alert settings. Copy config.yml.example to the normal operator configuration location and set any values you want to publish:

OPERATOR-WEBSITE: "https://operator.example/"
OPERATOR-EMAIL: "operator@example.org"
FEDIVERSE-OPERATOR-ID: "@operator@social.example"
FEDIVERSE-OPERATOR-URL: "https://social.example/@operator"
SUPPORT:
  - title: "Liberapay"
    url: "https://liberapay.com/example/"
  - title: "Bitcoin"
    value: "bc1qexample"

Empty values are omitted. SUPPORT is optional and renders a collapsed, presentation-only Support this directory block near the top of the human page. Each support entry has a title and exactly one HTTPS URL or plain-text value. The Fediverse ID and profile URL must be supplied together. See docs/CONFIGURATION.md for validation rules and alternate configuration paths.

API compatibility

/v1/relays retains the version 1 public-listing contract used by Activity-Relay Directory 1.0. Newer directory information, including the four public tiers and independent reachability evidence, is exposed through /v2/relays rather than changing the V1 response shape.

Protocol and API compatibility details live in docs/PROTOCOL.md and docs/PUBLIC-LISTING.md.

Security and privacy

The project is intentionally conservative about network access and stored data:

  • relay actor and signing-key retrieval uses SSRF-resistant DNS/address and redirect checks rather than the default HTTP client;
  • lifecycle requests require authenticated signatures, content digests, limited timestamp windows, and nonce replay protection;
  • moderation and operator provenance stay private;
  • the database contains relay-instance state, not connected-site or user lists;
  • public HTTP requests cannot start reachability, pruning, or retention work;
  • SQLite is intended for one active directory process on one host; and
  • public listing, lifecycle, reachability, and pruning all default to off.

See SECURITY.md and docs/SECURITY.md for the complete security model.

Development

go test -count=1 ./...
go test -race -count=1 ./...
go build ./cmd/activity-relay-directory

For a container build:

docker build --pull=false --build-arg VERSION=development .

Host-neutral Nginx, Apache, and Caddy examples are under contrib/.

Documentation

Operator and implementation details are split into topic-specific documents:

ARCHITECTURE.md, AGENTS.md, and TODO.md are maintainer-oriented references and are not required to operate a normal directory instance.

Package and data safety

The service does not automatically delete unavailable relays merely because they remain offline. Graveyard entries continue to receive periodic recovery checks. Hard deletion of inactive database records is a separate, explicitly configured retention operation and defaults to disabled.

For packaged installations, normal upgrade and removal preserve directory state. Explicit package purge is destructive. Always back up the SQLite database before purging or deliberately enabling hard retention.

Licence

GNU Affero General Public License version 3. See LICENCE.

Maintenance transparency

Development may use AI-assisted tooling for drafting, analysis, testing, and review support. A human maintainer reviews and approves changes, runs release checks, controls deployments, and remains accountable for the project.