Skip to content

Commit 31c324f

Browse files
committed
Add relay manual
1 parent 30c889e commit 31c324f

2 files changed

Lines changed: 370 additions & 0 deletions

File tree

docs/.vitepress/config.mts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,7 @@ const MANUAL = {
7575
{ text: "Key–value store", link: "/manual/kv.md" },
7676
{ text: "Message queue", link: "/manual/mq.md" },
7777
{ text: "Integration", link: "/manual/integration.md" },
78+
{ text: "Relay", link: "/manual/relay.md" },
7879
{ text: "Testing", link: "/manual/test.md" },
7980
{ text: "Linting", link: "/manual/lint.md" },
8081
{ text: "Logging", link: "/manual/log.md" },
@@ -98,6 +99,7 @@ const REFERENCES = {
9899
{ text: "@fedify/koa", link: "https://jsr.io/@fedify/koa/doc" },
99100
{ text: "@fedify/postgres", link: "https://jsr.io/@fedify/postgres/doc" },
100101
{ text: "@fedify/redis", link: "https://jsr.io/@fedify/redis/doc" },
102+
{ text: "@fedify/relay", link: "https://jsr.io/@fedify/relay/doc" },
101103
{ text: "@fedify/sqlite", link: "https://jsr.io/@fedify/sqlite/doc" },
102104
{ text: "@fedify/sveltekit", link: "https://jsr.io/@fedify/sveltekit/doc" },
103105
{ text: "@fedify/testing", link: "https://jsr.io/@fedify/testing/doc" },

docs/manual/relay.md

Lines changed: 368 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,368 @@
1+
---
2+
description: >-
3+
Fedify provides a ready-to-use relay server implementation for building
4+
ActivityPub relay infrastructure.
5+
---
6+
7+
Relay server
8+
============
9+
10+
*This API is available since Fedify 2.0.0.*
11+
12+
Fedify provides the `@fedify/relay` package for building [ActivityPub relay
13+
servers]—services that forward activities between instances without requiring
14+
individual actor-following relationships.
15+
16+
[ActivityPub relay servers]: https://fediverse.party/en/miscellaneous/#relays
17+
18+
19+
Setting up a relay server
20+
-------------------------
21+
22+
First, install the `@fedify/relay` package.
23+
24+
::: code-group
25+
26+
~~~~ sh [Deno]
27+
deno add @fedify/relay
28+
~~~~
29+
30+
~~~~ sh [Node.js]
31+
npm add @fedify/relay
32+
~~~~
33+
34+
~~~~ sh [Bun]
35+
bun add @fedify/relay
36+
~~~~
37+
38+
:::
39+
40+
Then create a relay using the `createRelay()` function.
41+
42+
~~~~ typescript twoslash
43+
import { createRelay } from "@fedify/relay";
44+
import { MemoryKvStore } from "@fedify/fedify";
45+
46+
const relay = createRelay("mastodon", {
47+
kv: new MemoryKvStore(),
48+
domain: "relay.example.com",
49+
name: "My ActivityPub Relay",
50+
subscriptionHandler: async (ctx, actor) => {
51+
// Approve all subscriptions
52+
return true;
53+
},
54+
});
55+
56+
Deno.serve((request) => relay.fetch(request));
57+
~~~~
58+
59+
> [!WARNING]
60+
> `MemoryKvStore` is for development only. For production, use a persistent
61+
> store like `RedisKvStore` from [`@fedify/redis`], `PostgresKvStore` from
62+
> [`@fedify/postgres`], or `DenoKvStore` from [`@fedify/denokv`].
63+
>
64+
> See the [*Key-value store* section](./kv.md) for details.
65+
66+
[`@fedify/redis`]: https://github.com/fedify-dev/fedify/tree/main/packages/redis
67+
[`@fedify/postgres`]: https://github.com/fedify-dev/fedify/tree/main/packages/postgres
68+
[`@fedify/denokv`]: https://github.com/fedify-dev/fedify/tree/main/packages/denokv
69+
70+
71+
Configuration options
72+
---------------------
73+
74+
`kv` (required)
75+
: A [`KvStore`](./kv.md) for storing subscriber information and cryptographic
76+
keys.
77+
78+
`domain`
79+
: The domain name where the relay is hosted. Defaults to `"localhost"`.
80+
81+
`name`
82+
: Display name for the relay actor. Defaults to `"ActivityPub Relay"`.
83+
84+
`queue`
85+
: A [`MessageQueue`](./mq.md) for background activity processing. Recommended
86+
for production:
87+
88+
~~~~ typescript twoslash
89+
import { createRelay } from "@fedify/relay";
90+
import { MemoryKvStore, InProcessMessageQueue } from "@fedify/fedify";
91+
// ---cut-before---
92+
const relay = createRelay("mastodon", {
93+
kv: new MemoryKvStore(),
94+
domain: "relay.example.com",
95+
queue: new InProcessMessageQueue(),
96+
subscriptionHandler: async (ctx, actor) => true,
97+
});
98+
~~~~
99+
100+
> [!NOTE]
101+
> For production, use [`RedisMessageQueue`] or [`PostgresMessageQueue`].
102+
103+
[`RedisMessageQueue`]: https://jsr.io/@fedify/redis/doc/mq/~/RedisMessageQueue
104+
[`PostgresMessageQueue`]: https://jsr.io/@fedify/postgres/doc/mq/~/PostgresMessageQueue
105+
106+
`subscriptionHandler` (required)
107+
: Callback to approve or reject subscription requests. See
108+
[*Handling subscriptions*](#handling-subscriptions). To create an open relay that accepts all subscriptions:
109+
110+
~~~~ typescript
111+
subscriptionHandler: async (ctx, actor) => true
112+
~~~~
113+
114+
`documentLoaderFactory`
115+
: A factory function for creating a document loader to fetch remote
116+
ActivityPub objects. See [*Getting a `Federation`
117+
object*](./federation.md#documentloaderfactory).
118+
119+
`authenticatedDocumentLoaderFactory`
120+
: A factory function for creating an authenticated document loader.
121+
See [`authenticatedDocumentLoaderFactory`](./federation.md#authenticateddocumentloaderfactory).
122+
123+
124+
Relay types
125+
-----------
126+
127+
The first parameter to `createRelay()` specifies the relay protocol:
128+
129+
| Feature | `"mastodon"` | `"litepub"` |
130+
|---------|--------------|-------------|
131+
| Activity forwarding | Direct | Wrapped in `Announce` |
132+
| Following relationship | One-way | Bidirectional |
133+
| Subscription state | Immediate `"accepted"` | `"pending"``"accepted"` |
134+
| Compatibility | Broad (most implementations) | LitePub-aware servers |
135+
136+
> [!TIP]
137+
> Use `"mastodon"` for broader compatibility. Switch to `"litepub"` only if
138+
> you need its specific features.
139+
140+
### Mastodon-style relay
141+
142+
Activities are forwarded directly to subscribers. Instances follow the relay,
143+
but the relay doesn't follow back.
144+
145+
~~~~ typescript twoslash
146+
import { createRelay } from "@fedify/relay";
147+
import { MemoryKvStore } from "@fedify/fedify";
148+
// ---cut-before---
149+
const relay = createRelay("mastodon", {
150+
kv: new MemoryKvStore(),
151+
domain: "relay.example.com",
152+
subscriptionHandler: async (ctx, actor) => true,
153+
});
154+
~~~~
155+
156+
Forwards `Create`, `Update`, `Delete`, `Move`, and `Announce` activities.
157+
158+
### LitePub-style relay
159+
160+
The relay server follows back instances that subscribe to it. Forwarded
161+
activities are wrapped in `Announce` objects.
162+
163+
~~~~ typescript twoslash
164+
import { createRelay } from "@fedify/relay";
165+
import { MemoryKvStore } from "@fedify/fedify";
166+
// ---cut-before---
167+
const relay = createRelay("litepub", {
168+
kv: new MemoryKvStore(),
169+
domain: "relay.example.com",
170+
subscriptionHandler: async (ctx, actor) => true,
171+
});
172+
~~~~
173+
174+
175+
Handling subscriptions
176+
----------------------
177+
178+
The `subscriptionHandler` is required and determines whether to approve or
179+
reject subscription requests. For an open relay that accepts all subscriptions:
180+
181+
~~~~ typescript twoslash
182+
import { createRelay } from "@fedify/relay";
183+
import { MemoryKvStore } from "@fedify/fedify";
184+
// ---cut-before---
185+
const relay = createRelay("mastodon", {
186+
kv: new MemoryKvStore(),
187+
domain: "relay.example.com",
188+
subscriptionHandler: async (ctx, actor) => true, // Accept all
189+
});
190+
~~~~
191+
192+
To implement approval logic with blocklists:
193+
194+
~~~~ typescript twoslash
195+
import { createRelay } from "@fedify/relay";
196+
import { MemoryKvStore } from "@fedify/fedify";
197+
// ---cut-before---
198+
const blockedDomains = ["spam.example", "blocked.example"];
199+
200+
const relay = createRelay("mastodon", {
201+
kv: new MemoryKvStore(),
202+
domain: "relay.example.com",
203+
subscriptionHandler: async (ctx, actor) => {
204+
const domain = new URL(actor.id!).hostname;
205+
if (blockedDomains.includes(domain)) {
206+
return false; // Reject
207+
}
208+
return true; // Approve
209+
},
210+
});
211+
~~~~
212+
213+
The handler receives:
214+
215+
- `ctx`: The `Context<RelayOptions>` object
216+
- `actor`: The `Actor` requesting subscription
217+
218+
Return `true` to approve or `false` to reject. Rejected requests receive a
219+
`Reject` activity.
220+
221+
222+
Managing followers
223+
------------------
224+
225+
Follower data is stored in the [`KvStore`](./kv.md) with keys following the
226+
pattern `["follower", actorId]`. Each entry contains:
227+
228+
- `actor`: The actor's JSON-LD data
229+
- `state`: Either `"pending"` or `"accepted"`
230+
231+
### Querying followers
232+
233+
~~~~ typescript twoslash
234+
import type { KvStore } from "@fedify/fedify";
235+
const kv = null as unknown as KvStore;
236+
// ---cut-before---
237+
import type { RelayFollower } from "@fedify/relay";
238+
239+
for await (const entry of kv.list<RelayFollower>(["follower"])) {
240+
console.log(`Follower: ${entry.value.actor["@id"]}`);
241+
console.log(`State: ${entry.value.state}`);
242+
}
243+
~~~~
244+
245+
> [!NOTE]
246+
> The `~KvStore.list()` method requires a `KvStore` implementation that
247+
> supports listing by prefix (Redis, PostgreSQL, SQLite, Deno KV all support
248+
> this).
249+
250+
### Validating follower objects
251+
252+
~~~~ typescript twoslash
253+
import type { KvStore } from "@fedify/fedify";
254+
const kv = null as unknown as KvStore;
255+
// ---cut-before---
256+
import { isRelayFollower } from "@fedify/relay";
257+
258+
for await (const entry of kv.list(["follower"])) {
259+
if (isRelayFollower(entry.value)) {
260+
console.log(`Valid follower in state: ${entry.value.state}`);
261+
}
262+
}
263+
~~~~
264+
265+
266+
Storage requirements
267+
--------------------
268+
269+
### Follower data
270+
271+
Stored with keys `["follower", actorId]`. Actor objects typically range from
272+
1–10 KB. For 1,000 subscribers, expect 1–10 MB of storage.
273+
274+
### Cryptographic keys
275+
276+
Two key pairs are generated and stored:
277+
278+
| Key | Purpose |
279+
|-----|---------|
280+
| `["keypair", "rsa", "relay"]` | HTTP Signatures |
281+
| `["keypair", "ed25519", "relay"]` | Linked Data Signatures, Object Integrity Proofs |
282+
283+
> [!NOTE]
284+
> These keys are critical for the relay's identity. Back up your `KvStore`
285+
> regularly.
286+
287+
288+
Security considerations
289+
-----------------------
290+
291+
### Signature verification
292+
293+
The relay automatically verifies incoming activities using:
294+
295+
- [HTTP Signatures]
296+
- [Linked Data Signatures]
297+
- [Object Integrity Proofs]
298+
299+
Invalid signatures are silently ignored. Enable [logging](./log.md) for the
300+
`["fedify", "sig"]` category to debug verification failures.
301+
302+
[HTTP Signatures]: https://datatracker.ietf.org/doc/html/draft-cavage-http-signatures-12
303+
[Linked Data Signatures]: https://web.archive.org/web/20170923124140/https://w3c-dvcg.github.io/ld-signatures/
304+
[Object Integrity Proofs]: https://w3id.org/fep/8b32
305+
306+
### Subscription abuse
307+
308+
Protect against abuse by:
309+
310+
1. Implementing a `subscriptionHandler` to validate requests
311+
2. Maintaining a blocklist
312+
3. Rate limiting at the infrastructure level
313+
4. Monitoring activity volumes
314+
315+
### Content moderation
316+
317+
> [!WARNING]
318+
> Running a relay makes you responsible for forwarded content. Establish clear
319+
> policies and vet subscribing instances.
320+
321+
### Privacy
322+
323+
The relay has access to all activities that pass through it. Do not store or
324+
log activity content beyond operational needs.
325+
326+
> [!CAUTION]
327+
> Never forward non-public activities. The relay is designed only for public
328+
> content distribution.
329+
330+
331+
Monitoring
332+
----------
333+
334+
### Logging
335+
336+
Enable relay-specific logging:
337+
338+
~~~~ typescript twoslash
339+
import { configure, getConsoleSink } from "@logtape/logtape";
340+
341+
await configure({
342+
sinks: { console: getConsoleSink() },
343+
loggers: [
344+
{ category: ["fedify"], level: "info", sinks: ["console"] },
345+
],
346+
});
347+
~~~~
348+
349+
Key log categories:
350+
351+
| Category | Description |
352+
|----------|-------------|
353+
| `["fedify", "federation", "inbox"]` | Incoming activities |
354+
| `["fedify", "federation", "outbox"]` | Outgoing activities |
355+
| `["fedify", "sig"]` | Signature verification |
356+
357+
### OpenTelemetry
358+
359+
The relay supports [OpenTelemetry](./opentelemetry.md) tracing. Key spans:
360+
361+
| Span | Description |
362+
|------|-------------|
363+
| `activitypub.inbox` | Receiving activities |
364+
| `activitypub.send_activity` | Forwarding activities |
365+
| `activitypub.dispatch_inbox_listener` | Processing inbox events |
366+
367+
368+
<!-- cSpell: ignore LitePub -->

0 commit comments

Comments
 (0)