This package is available since Fedify 2.0.0.
This package provides ActivityPub relay functionality for the Fedify ecosystem, enabling the creation and management of relay servers that can forward activities between federated instances.
For comprehensive documentation on building and operating relay servers, see the Relay server section in the Fedify manual.
ActivityPub relays are infrastructure components that help small instances participate effectively in the federated social network by acting as intermediary servers that distribute public content without requiring individual actor-following relationships. When an instance subscribes to a relay, all public posts from that instance are forwarded to all other subscribed instances, creating a shared pool of federated content.
This package implements the relay-server side of the two relay protocols described by FEP-ae0c. It does not subscribe an existing ActivityPub application to remote relays.
Mastodon-style clients subscribe by sending a Follow whose object is the
ActivityStreams Public collection to the relay's shared inbox. The relay
accepts or rejects the subscription and forwards signed activities directly to
accepted subscribers.
- Direct forwarding of
Create,Update,Delete,Move, andAnnounceactivities - Immediate subscription state after approval
- Subscription through the shared inbox URI
LitePub-style clients subscribe by following the relay actor. After approving
the request, the relay follows the client actor back and waits for an Accept.
It wraps forwarded objects in Announce activities.
- Reciprocal following between relay and subscribers
- Activities distributed through
Announce - Two-phase subscription (pending → accepted)
- Subscription through the relay actor URI
::: code-group
deno add jsr:@fedify/relaynpm add @fedify/relaypnpm add @fedify/relayyarn add @fedify/relaybun add @fedify/relay:::
Here's a simple example of creating a relay server using the factory function.
import { createRelay } from "@fedify/relay";
import { MemoryKvStore } from "@fedify/fedify";
// Create a Mastodon-style relay
const relay = createRelay("mastodon", {
kv: new MemoryKvStore(),
origin: "https://relay.example.com",
// Required: Set a subscription handler to approve/reject subscriptions
subscriptionHandler: async (ctx, actor) => {
// For an open relay, simply return true
// return true;
// Or implement custom approval logic:
const domain = new URL(actor.id!).hostname;
const blockedDomains = ["spam.example", "blocked.example"];
return !blockedDomains.includes(domain);
},
});
// Serve the relay
Deno.serve((request) => relay.fetch(request));You can also create a LitePub-style relay by changing the type.
const relay = createRelay("litepub", {
kv: new MemoryKvStore(),
origin: "https://relay.example.com",
subscriptionHandler: async (ctx, actor) => true,
});The relay actor and shared inbox use fixed internal routes. Retrieve their public URIs from the relay instead of constructing paths yourself.
const actorUri = await relay.getActorUri();
// https://relay.example.com/users/relay
const inboxUri = await relay.getSharedInboxUri();
// https://relay.example.com/inboxGive Mastodon-style clients the shared inbox URI and LitePub-style clients the actor URI.
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
});You can also implement custom approval logic.
const relay = createRelay("mastodon", {
kv: new MemoryKvStore(),
origin: "https://relay.example.com",
subscriptionHandler: async (ctx, actor) => {
// Example: Only allow subscriptions from specific domains
const domain = new URL(actor.id!).hostname;
const allowedDomains = ["mastodon.social", "fosstodon.org"];
return allowedDomains.includes(domain);
},
});The relay provides methods to query and manage followers without exposing internal storage details.
for await (const follower of relay.listFollowers()) {
console.log(`Follower: ${follower.actorId}`);
console.log(`State: ${follower.state}`);
console.log(`Actor name: ${follower.actor.name}`);
console.log(`Actor type: ${follower.actor.constructor.name}`);
}const follower = await relay.getFollower("https://mastodon.example.com/users/alice");
if (follower) {
console.log(`Found follower in state: ${follower.state}`);
console.log(`Actor username: ${follower.actor.preferredUsername}`);
console.log(`Inbox: ${follower.actor.inboxId?.href}`);
} else {
console.log("Follower not found");
}The relay's fetch() method returns a standard Response object, making it
compatible with any web framework that supports the Fetch API. Here's an
example with Hono.
import { Hono } from "hono";
import { createRelay } from "@fedify/relay";
import { MemoryKvStore } from "@fedify/fedify";
const app = new Hono();
const relay = createRelay("mastodon", {
kv: new MemoryKvStore(),
origin: "https://relay.example.com",
subscriptionHandler: async (ctx, actor) => true,
});
app.use("*", async (c) => {
return await relay.fetch(c.req.raw);
});
export default app;- Actor registration—the relay presents itself as an
Applicationactor at/users/relay. - Subscription—Mastodon-style clients follow the Public collection; LitePub-style clients follow the relay actor.
- Approval—the relay's subscription handler determines whether to approve
the subscription and responds with
AcceptorReject. - Forwarding—the relay handles
Create,Update,Delete,Move, andAnnounceactivities delivered to its inbox. Mastodon-style relays forward them directly; LitePub-style relays wrap their objects inAnnounce. - Unsubscription—instances can unsubscribe by sending an
Undoactivity wrapping their originalFollowactivity.
createRelay() provides the relay-specific ActivityPub routes and behavior.
The surrounding application remains responsible for HTTPS, persistent storage,
a durable production queue, subscription policy, rate limiting, monitoring,
and moderation. WebFinger and NodeInfo discovery endpoints are not configured
by this package.
The subscriptionHandler controls which actors become delivery recipients; it
does not authorize publishing to the relay. The relay does not require an
activity sender to be a stored follower or inspect its audience for the Public
collection, so deployments need to account for that behavior in their access
and moderation policies.
The relay requires a key–value store to persist the following data.
- Subscriber actor information and subscription state
- The relay's cryptographic key pairs (RSA and Ed25519)
Any KvStore implementation from Fedify can be used, including the following.
MemoryKvStore(for development/testing)DenoKvStore(Deno KV)RedisKvStore(Redis)PostgresKvStore(PostgreSQL)MysqlKvStore(MySQL/MariaDB)SqliteKvStore(SQLite)
For production use, choose a persistent storage backend like Redis, PostgreSQL, or MySQL/MariaDB. See the Fedify documentation on key–value stores for more details.
Factory function to create a relay instance.
function createRelay(
type: "mastodon" | "litepub",
options: RelayOptions
): RelayParameters:
type: The type of relay to create ("mastodon"or"litepub")options: Configuration options for the relay
Returns: A Relay instance
Public interface for ActivityPub relay implementations.
fetch(request: Request): Promise<Response>: Handle incoming HTTP requestslistFollowers(): AsyncIterableIterator<RelayFollower>: Lists all followers of the relaygetFollower(actorId: string): Promise<RelayFollower | null>: Gets a specific follower by actor IDgetActorUri(): Promise<URL>: Gets the URI of the relay actorgetSharedInboxUri(): Promise<URL>: Gets the shared inbox URI of the relay
The relay type is specified when calling createRelay().
"mastodon": Mastodon-compatible relay using direct activity forwarding, immediate subscription approval, and LD signatures"litepub": LitePub-compatible relay using bidirectional following, activities wrapped inAnnounce, and two-phase subscription
Configuration options for the relay.
kv: KvStore(required): Key–value store for persisting relay dataorigin: string(required): Relay's origin URL (e.g.,"https://relay.example.com")name?: string: Relay's display name (defaults to"ActivityPub Relay")subscriptionHandler: SubscriptionRequestHandler(required): Handler for subscription approval/rejectiondocumentLoaderFactory?: DocumentLoaderFactory: Custom document loader factoryauthenticatedDocumentLoaderFactory?: AuthenticatedDocumentLoaderFactory: Custom authenticated document loader factoryqueue?: MessageQueue: Message queue for background activity processing
A function that determines whether to approve a subscription request.
type SubscriptionRequestHandler = (
ctx: Context<RelayOptions>,
clientActor: Actor,
) => Promise<boolean>Parameters:
ctx: The Fedify context object with relay optionsclientActor: The actor requesting to subscribe
Returns:
trueto approve the subscriptionfalseto reject the subscription
A follower of the relay with validated Actor instance.
interface RelayFollower {
readonly actorId: string;
readonly actor: Actor;
readonly state: "pending" | "accepted";
}Properties:
actorId: The actor ID (URL) of the followeractor: The validated Actor objectstate: The follower's state ("pending"or"accepted")