Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions docs/.vitepress/config.mts
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,7 @@ const MANUAL = {
{ text: "Key–value store", link: "/manual/kv.md" },
{ text: "Message queue", link: "/manual/mq.md" },
{ text: "Integration", link: "/manual/integration.md" },
{ text: "Relay", link: "/manual/relay.md" },
{ text: "Testing", link: "/manual/test.md" },
{ text: "Linting", link: "/manual/lint.md" },
{ text: "Logging", link: "/manual/log.md" },
Expand All @@ -98,6 +99,7 @@ const REFERENCES = {
{ text: "@fedify/koa", link: "https://jsr.io/@fedify/koa/doc" },
{ text: "@fedify/postgres", link: "https://jsr.io/@fedify/postgres/doc" },
{ text: "@fedify/redis", link: "https://jsr.io/@fedify/redis/doc" },
{ text: "@fedify/relay", link: "https://jsr.io/@fedify/relay/doc" },
{ text: "@fedify/sqlite", link: "https://jsr.io/@fedify/sqlite/doc" },
{ text: "@fedify/sveltekit", link: "https://jsr.io/@fedify/sveltekit/doc" },
{ text: "@fedify/testing", link: "https://jsr.io/@fedify/testing/doc" },
Expand Down
368 changes: 368 additions & 0 deletions docs/manual/relay.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,368 @@
---
description: >-
Fedify provides a ready-to-use relay server implementation for building
ActivityPub relay infrastructure.
---

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.

[ActivityPub relay servers]: https://fediverse.party/en/miscellaneous/#relays


Setting up a relay server
-------------------------

First, install the `@fedify/relay` package.

::: code-group

~~~~ sh [Deno]
deno add @fedify/relay
~~~~

~~~~ sh [Node.js]
npm add @fedify/relay
~~~~

~~~~ sh [Bun]
bun add @fedify/relay
~~~~

:::

Then create a relay using the `createRelay()` function.

~~~~ typescript twoslash
import { createRelay } from "@fedify/relay";
import { MemoryKvStore } from "@fedify/fedify";

const relay = createRelay("mastodon", {
kv: new MemoryKvStore(),
domain: "relay.example.com",
name: "My ActivityPub Relay",
subscriptionHandler: async (ctx, actor) => {
// Approve all subscriptions
return true;
},
});

Deno.serve((request) => relay.fetch(request));
~~~~

> [!WARNING]
> `MemoryKvStore` is for development only. For production, use a persistent
> store like `RedisKvStore` from [`@fedify/redis`], `PostgresKvStore` from
> [`@fedify/postgres`], or `DenoKvStore` from [`@fedify/denokv`].
>
> See the [*Key-value store* section](./kv.md) for details.

[`@fedify/redis`]: https://github.com/fedify-dev/fedify/tree/main/packages/redis
[`@fedify/postgres`]: https://github.com/fedify-dev/fedify/tree/main/packages/postgres
[`@fedify/denokv`]: https://github.com/fedify-dev/fedify/tree/main/packages/denokv


Configuration options
---------------------

`kv` (required)
: A [`KvStore`](./kv.md) for storing subscriber information and cryptographic
keys.

`domain`
: The domain name where the relay is hosted. Defaults to `"localhost"`.

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

`queue`
: A [`MessageQueue`](./mq.md) for background activity processing. Recommended
for production:

~~~~ typescript twoslash
import { createRelay } from "@fedify/relay";
import { MemoryKvStore, InProcessMessageQueue } from "@fedify/fedify";
// ---cut-before---
const relay = createRelay("mastodon", {
kv: new MemoryKvStore(),
domain: "relay.example.com",
queue: new InProcessMessageQueue(),
subscriptionHandler: async (ctx, actor) => true,
});
~~~~

> [!NOTE]
> For production, use [`RedisMessageQueue`] or [`PostgresMessageQueue`].

[`RedisMessageQueue`]: https://jsr.io/@fedify/redis/doc/mq/~/RedisMessageQueue
[`PostgresMessageQueue`]: https://jsr.io/@fedify/postgres/doc/mq/~/PostgresMessageQueue

`subscriptionHandler` (required)
: Callback to approve or reject subscription requests. See
[*Handling subscriptions*](#handling-subscriptions). To create an open relay that accepts all subscriptions:

~~~~ typescript
subscriptionHandler: async (ctx, actor) => true
~~~~

`documentLoaderFactory`
: A factory function for creating a document loader to fetch remote
ActivityPub objects. See [*Getting a `Federation`
object*](./federation.md#documentloaderfactory).

`authenticatedDocumentLoaderFactory`
: A factory function for creating an authenticated document loader.
See [`authenticatedDocumentLoaderFactory`](./federation.md#authenticateddocumentloaderfactory).


Relay types
-----------

The first parameter to `createRelay()` specifies the relay protocol:

| Feature | `"mastodon"` | `"litepub"` |
|---------|--------------|-------------|
| Activity forwarding | Direct | Wrapped in `Announce` |
| Following relationship | One-way | Bidirectional |
| Subscription state | Immediate `"accepted"` | `"pending"` → `"accepted"` |
| Compatibility | Broad (most implementations) | LitePub-aware servers |

> [!TIP]
> Use `"mastodon"` for broader compatibility. Switch to `"litepub"` only if
> you need its specific features.

### Mastodon-style relay

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

~~~~ typescript twoslash
import { createRelay } from "@fedify/relay";
import { MemoryKvStore } from "@fedify/fedify";
// ---cut-before---
const relay = createRelay("mastodon", {
kv: new MemoryKvStore(),
domain: "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.

~~~~ typescript twoslash
import { createRelay } from "@fedify/relay";
import { MemoryKvStore } from "@fedify/fedify";
// ---cut-before---
const relay = createRelay("litepub", {
kv: new MemoryKvStore(),
domain: "relay.example.com",
subscriptionHandler: async (ctx, actor) => true,
});
~~~~


Handling subscriptions
----------------------

The `subscriptionHandler` is required and determines whether to approve or
reject subscription requests. For an open relay that accepts all subscriptions:

~~~~ typescript twoslash
import { createRelay } from "@fedify/relay";
import { MemoryKvStore } from "@fedify/fedify";
// ---cut-before---
const relay = createRelay("mastodon", {
kv: new MemoryKvStore(),
domain: "relay.example.com",
subscriptionHandler: async (ctx, actor) => true, // Accept all
});
~~~~

To implement approval logic with blocklists:

~~~~ typescript twoslash
import { createRelay } from "@fedify/relay";
import { MemoryKvStore } from "@fedify/fedify";
// ---cut-before---
const blockedDomains = ["spam.example", "blocked.example"];

const relay = createRelay("mastodon", {
kv: new MemoryKvStore(),
domain: "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:

- `ctx`: The `Context<RelayOptions>` object
- `actor`: The `Actor` requesting subscription

Return `true` to approve or `false` to reject. Rejected requests receive a
`Reject` activity.


Managing followers
------------------

Follower data is stored in the [`KvStore`](./kv.md) with keys following the
pattern `["follower", actorId]`. Each entry contains:

- `actor`: The actor's JSON-LD data
- `state`: Either `"pending"` or `"accepted"`

### Querying followers

~~~~ typescript twoslash
import type { KvStore } from "@fedify/fedify";
const kv = null as unknown as KvStore;
// ---cut-before---
import type { RelayFollower } from "@fedify/relay";

for await (const entry of kv.list<RelayFollower>(["follower"])) {
console.log(`Follower: ${entry.value.actor["@id"]}`);
console.log(`State: ${entry.value.state}`);
}
~~~~

> [!NOTE]
> The `~KvStore.list()` method requires a `KvStore` implementation that
> supports listing by prefix (Redis, PostgreSQL, SQLite, Deno KV all support
> this).

### Validating follower objects

~~~~ typescript twoslash
import type { KvStore } from "@fedify/fedify";
const kv = null as unknown as KvStore;
// ---cut-before---
import { isRelayFollower } from "@fedify/relay";

for await (const entry of kv.list(["follower"])) {
if (isRelayFollower(entry.value)) {
console.log(`Valid follower in state: ${entry.value.state}`);
}
}
~~~~
Comment on lines +231 to +263
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The Managing followers section exposes internal implementation details (key patterns like ["follower", actorId] and the raw RelayFollower data structure). This approach has a few concerns:

  1. Encapsulation: Exposing the internal key schema makes it part of the public API contract
  2. Maintenance burden: Changing the storage structure later would become a breaking change
  3. Incomplete API: Direct KvStore access isn't a proper API—it lacks type safety and discoverability

I think we have two options:

  • Option A: Remove this section from the documentation for now, and add it back once we have proper APIs like relay.listFollowers(), relay.getFollower(actorId), etc.

  • Option B: Add these methods to BaseRelay first, then document the proper API instead of raw KvStore access.

What do you think?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Option B sounds good, I will be working on that with another PR. (relay.listFollowers() and relay.getFollower(actorId)) How's that sound?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good!



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

Two key pairs are generated and stored:

| Key | Purpose |
|-----|---------|
| `["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

The relay automatically verifies incoming activities using:

- [HTTP Signatures]
- [Linked Data Signatures]
- [Object Integrity Proofs]

Invalid signatures are silently ignored. Enable [logging](./log.md) for the
`["fedify", "sig"]` category to debug verification failures.

[HTTP Signatures]: https://datatracker.ietf.org/doc/html/draft-cavage-http-signatures-12
[Linked Data Signatures]: https://web.archive.org/web/20170923124140/https://w3c-dvcg.github.io/ld-signatures/
[Object Integrity Proofs]: https://w3id.org/fep/8b32

### Subscription abuse

Protect against abuse by:

1. Implementing a `subscriptionHandler` to validate requests
2. Maintaining a blocklist
3. Rate limiting at the infrastructure level
4. Monitoring activity volumes

### Content moderation

> [!WARNING]
> Running a relay makes you responsible for forwarded content. Establish clear
> policies and vet subscribing instances.

### Privacy

The relay has access to all activities that pass through it. Do not store or
log activity content beyond operational needs.

> [!CAUTION]
> Never forward non-public activities. The relay is designed only for public
> content distribution.


Monitoring
----------

### Logging

Enable relay-specific logging:

~~~~ typescript twoslash
import { configure, getConsoleSink } from "@logtape/logtape";

await configure({
sinks: { console: getConsoleSink() },
loggers: [
{ category: ["fedify"], level: "info", sinks: ["console"] },
],
});
~~~~

Key log categories:

| Category | Description |
|----------|-------------|
| `["fedify", "federation", "inbox"]` | Incoming activities |
| `["fedify", "federation", "outbox"]` | Outgoing activities |
| `["fedify", "sig"]` | Signature verification |

### OpenTelemetry

The relay supports [OpenTelemetry](./opentelemetry.md) tracing. Key spans:

| Span | Description |
|------|-------------|
| `activitypub.inbox` | Receiving activities |
| `activitypub.send_activity` | Forwarding activities |
| `activitypub.dispatch_inbox_listener` | Processing inbox events |


<!-- cSpell: ignore LitePub -->