Skip to content

Relay

This API is available since Fedify 2.0.0.

Fedify provides the @fedify/relay package for building ActivityPub relay servers—services that forward activities between instances without requiring individual actor-following relationships.

Setting up a relay server

First, install the @fedify/relay package.

deno add @fedify/relay
npm add @fedify/relay @hono/node-server
pnpm add @fedify/relay @hono/node-server
yarn add @fedify/relay @hono/node-server
bun add @fedify/relay

Then create a relay using the createRelay() function.

import { 
createRelay
} from "@fedify/relay";
import {
MemoryKvStore
} from "@fedify/fedify";
const
relay
=
createRelay
("mastodon", {
kv
: new
MemoryKvStore
(),
origin
: "https://relay.example.com",
name
: "My ActivityPub Relay",
subscriptionHandler
: async (
ctx
,
actor
) => {
// Approve all subscriptions return true; }, }); Deno.
serve
((
request
) =>
relay
.
fetch
(
request
));
import { 
createRelay
} from "@fedify/relay";
import {
MemoryKvStore
} from "@fedify/fedify";
const
relay
=
createRelay
("mastodon", {
kv
: new
MemoryKvStore
(),
origin
: "https://relay.example.com",
name
: "My ActivityPub Relay",
subscriptionHandler
: async (
ctx
,
actor
) => {
// Approve all subscriptions return true; }, });
Bun
.
serve
({
port
: 8000,
fetch
(
request
) {
return
relay
.
fetch
(
request
);
}, });
import { 
createRelay
} from "@fedify/relay";
import {
MemoryKvStore
} from "@fedify/fedify";
import {
serve
} from "@hono/node-server";
const
relay
=
createRelay
("mastodon", {
kv
: new
MemoryKvStore
(),
origin
: "https://relay.example.com",
name
: "My ActivityPub Relay",
subscriptionHandler
: async (
ctx
,
actor
) => {
// Approve all subscriptions return true; }, });
serve
({
port
: 8000,
fetch
(
request
) {
return
relay
.
fetch
(
request
);
}, });

WARNING

MemoryKvStore is for development only. For production, use a persistent store like RedisKvStore from @fedify/redis, PostgresKvStore from @fedify/postgres, MysqlKvStore from @fedify/mysql, or DenoKvStore from @fedify/denokv.

See the Key–value store section for details.

Configuration options

kv (required)

A KvStore for storing subscriber information and cryptographic keys.

origin (required)

The origin URL where the relay is hosted (e.g., "https://relay.example.com").

name

Display name for the relay actor. Defaults to "ActivityPub Relay".

queue

A MessageQueue for background activity processing. Recommended for production.

const 
relay
=
createRelay
("mastodon", {
kv
: new
MemoryKvStore
(),
origin
: "https://relay.example.com",
queue
: new
InProcessMessageQueue
(),
subscriptionHandler
: async (
ctx
,
actor
) => true,
});
subscriptionHandler (required)

Callback to approve or reject subscription requests. See Handling subscriptions. To create an open relay that accepts all subscriptions, set subscriptionHandler to always return true.

subscriptionHandler: async (ctx, actor) => true
documentLoaderFactory

A factory function for creating a document loader to fetch remote ActivityPub objects. See Getting a Federation object.

authenticatedDocumentLoaderFactory

A factory function for creating an authenticated document loader. See authenticatedDocumentLoaderFactory.

Relay types

The first parameter to createRelay() selects how this relay server handles subscriptions and forwards activities. The package implements the server side of the Mastodon-style and LitePub-style protocols described by FEP-ae0c; it does not configure an existing ActivityPub application as a relay client.

Feature"mastodon""litepub"
Activity forwardingDirectWrapped in Announce
Following relationshipOne-wayBidirectional
Subscription stateImmediate "accepted""pending""accepted"
Canonical Follow objectPublic collectionRelay actor

Mastodon-style relay

Activities are forwarded directly to subscribers. Instances follow the relay, but the relay doesn't follow back.

const 
relay
=
createRelay
("mastodon", {
kv
: new
MemoryKvStore
(),
origin
: "https://relay.example.com",
subscriptionHandler
: async (
ctx
,
actor
) => true,
});

Forwards Create, Update, Delete, Move, and Announce activities.

LitePub-style relay

The relay server follows back instances that subscribe to it. Forwarded activities are wrapped in Announce objects.

const 
relay
=
createRelay
("litepub", {
kv
: new
MemoryKvStore
(),
origin
: "https://relay.example.com",
subscriptionHandler
: async (
ctx
,
actor
) => true,
});

Subscribing to a relay

Instance administrators can subscribe to your relay by adding the relay URL in their server settings. The URL format differs depending on the relay type.

Subscription URL

Retrieve subscription URLs from the relay instance rather than constructing them from assumed paths.

const 
actorUri
= await
relay
.
getActorUri
();
const
sharedInboxUri
= await
relay
.
getSharedInboxUri
();
Relay typeGive clientsDefault URI
"mastodon"sharedInboxUrihttps://relay.example.com/inbox
"litepub"actorUrihttps://relay.example.com/users/relay

For more details on the protocol differences, see FEP-ae0c.

Application responsibilities

createRelay() provides the relay actor, inboxes, subscription handshake, activity forwarding, cryptographic keys, and follower storage. The surrounding application still needs to implement the following.

  • Route requests for the configured origin to relay.fetch() without rewriting the relay's paths.
  • Terminate HTTPS and use a persistent KvStore in production.
  • Configure a durable MessageQueue when delivery should survive process restarts.
  • Implement subscription policy and infrastructure-level rate limiting, monitoring, and moderation.
  • Provide WebFinger or NodeInfo separately when deployed clients require those discovery endpoints.

The subscriptionHandler decides who is stored as a delivery recipient. It is not authorization for publishing to the relay. The relay verifies incoming activities using Fedify's federation pipeline, but it does not require the sender to be a stored follower or check that an activity addresses the Public collection. Deployments should account for that behavior in their access and moderation policies.

Handling subscriptions

The subscriptionHandler is required and determines whether to approve or reject subscription requests. The following example creates an open relay that accepts all subscriptions.

const 
relay
=
createRelay
("mastodon", {
kv
: new
MemoryKvStore
(),
origin
: "https://relay.example.com",
subscriptionHandler
: async (
ctx
,
actor
) => true, // Accept all
});

Approval logic can also be implemented with a domain block list.

const 
blockedDomains
= ["spam.example", "blocked.example"];
const
relay
=
createRelay
("mastodon", {
kv
: new
MemoryKvStore
(),
origin
: "https://relay.example.com",
subscriptionHandler
: async (
ctx
,
actor
) => {
const
domain
= new
URL
(
actor
.
id
!).
hostname
;
if (
blockedDomains
.
includes
(
domain
)) {
return false; // Reject } return true; // Approve }, });

The handler receives the Context<RelayOptions> object as ctx and the Actor requesting the subscription as actor.

Return true to approve the request or false to reject it. The relay responds to rejected requests with a Reject activity.

Managing followers

The relay provides methods to query and manage followers through the Relay interface.

Listing all followers

Use listFollowers() to iterate over all followers.

for await (const 
follower
of
relay
.
listFollowers
()) {
console
.
log
(`Follower: ${
follower
.
actorId
}`);
console
.
log
(`State: ${
follower
.
state
}`);
console
.
log
(`Actor name: ${
follower
.
actor
.
name
}`);
}

Getting a specific follower

Use getFollower() to retrieve a specific follower by actor ID.

const 
follower
= await
relay
.
getFollower
(
"https://mastodon.example.com/users/alice" ); if (
follower
!= null) {
console
.
log
(`State: ${
follower
.
state
}`);
console
.
log
(`Actor: ${
follower
.
actor
.
preferredUsername
}`);
}

RelayFollower type

Each follower entry contains the following.

  • actorId: The actor's ID (URL) as a string
  • actor: The validated Actor object
  • state: Either "pending" or "accepted"

NOTE

The listFollowers() method requires a KvStore implementation that supports listing by prefix (Redis, PostgreSQL, SQLite, Deno KV all support this).

Storage requirements

Follower data

Stored with keys ["follower", actorId]. Actor objects typically range from 1–10 KB. For 1,000 subscribers, expect 1–10 MB of storage.

Cryptographic keys

The relay generates and stores two key pairs.

KeyPurpose
["keypair", "rsa", "relay"]HTTP Signatures
["keypair", "ed25519", "relay"]Linked Data Signatures, Object Integrity Proofs

NOTE

These keys are critical for the relay's identity. Back up your KvStore regularly.

Security considerations

Signature verification

Incoming activities pass through Fedify's normal signature verification pipeline. A valid signature authenticates the sender but does not make the activity trusted.

The subscriptionHandler controls which actors receive forwarded activities. It does not restrict which actors can submit activities to the relay, and createRelay() does not check whether an activity addresses the Public collection before forwarding it.

Deployments should apply appropriate access controls, rate limiting, and moderation to the relay inbox. Avoid logging activity content unless it is needed for operation or debugging.

Monitoring

Logging

The following example enables Fedify logging, including relay operations.

import { 
configure
,
getConsoleSink
} from "@logtape/logtape";
await
configure
({
sinks
: {
console
:
getConsoleSink
() },
loggers
: [
{
category
: ["fedify"],
lowestLevel
: "info",
sinks
: ["console"] },
], });

You can enable logging relevant to relay operation as follows.

CategoryDescription
["fedify", "relay"]Relay-specific events
["fedify", "federation", "inbox"]Incoming activities
["fedify", "federation", "outbox"]Outgoing activities
["fedify", "sig"]Signature verification

OpenTelemetry

Relay operations are included in OpenTelemetry.

SpanDescription
activitypub.inboxReceiving an activity
activitypub.send_activitySending a relayed activity
activitypub.dispatch_inbox_listenerProcessing an inbox activity