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 supports two popular relay protocols used in the fediverse:
The Mastodon-style relay protocol uses LD signatures for activity verification and follows the Public collection. This protocol is widely supported by Mastodon and many other ActivityPub implementations.
Key features:
- Direct activity relaying with proper content types (
Create,Update,Delete,Move) - LD signature verification and generation
- Follows the ActivityPub Public collection
- Simple subscription mechanism via
Followactivities
The LitePub-style relay protocol uses bidirectional following relationships
and wraps activities in Announce activities for distribution.
Key features:
- Reciprocal following between relay and subscribers
- Activities wrapped in
Announcefor distribution - Two-phase subscription (pending → accepted)
- Enhanced federation capabilities
::: 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 subscriptionHandler is required and determines whether to approve or
reject subscription requests. For 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;The relay operates by:
- Actor registration: The relay presents itself as a Service actor at
/users/relay - Subscription: Instances subscribe to the relay by sending a
Followactivity - Approval: The relay's subscription handler determines whether to
approve the subscription (responds with
AcceptorReject) - Forwarding: When a subscribed instance sends activities (
Create,Update,Delete,Move) to the relay's inbox, the relay forwards them to all other subscribed instances - Unsubscription: Instances can unsubscribe by sending an
Undoactivity wrapping their originalFollowactivity
The relay requires a key–value store to persist:
- Subscriber list and their Follow activity IDs
- Subscriber actor information
- Relay's cryptographic key pairs (RSA and Ed25519)
Any KvStore implementation from Fedify can be used, including:
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")