Fedify is a TypeScript framework for building ActivityPub servers that participate in the fediverse. It reduces the complexity and boilerplate typically required for ActivityPub implementation whil...
Fedify is a TypeScript framework for building ActivityPub servers that participate in the fediverse. It reduces the complexity and boilerplate typically required for ActivityPub implementation while providing comprehensive federation capabilities.
We are thrilled to announce Fedify 2.0.0, the most significant release in Fedify's history. This major version brings a fundamentally restructured modular architecture, a real-time debug dashboard, ActivityPub relay support, ordered message delivery, permanent failure handling, and many more improvements across the entire ecosystem.
Fedify 2.0.0 is the culmination of months of collaborative effort from the Fedify community, including significant contributions from Korea's OSSCA (Open Source Contribution Academy) participants. This release includes breaking changes that require careful migration—please review the Migration guide section below.
Before diving into the new features, here is a summary of breaking changes that require attention when upgrading from Fedify 1.x:
contextLoader, documentLoader options, CreateFederationOptions, fetchDocumentLoader(), and { handle: string } parameter forms have been removed (Remove deprecated APIs for Fedify 2.0 #376).LanguageTag replaced by Intl.Locale: The LanguageString.language property is now LanguageString.locale of type Intl.Locale (Migrate from @phensley/language-tag to Intl.Locale for Fedify 2.0 #280, Migrate from LanguageTag to Intl.Locale for representing language tags #392).software.version is now string: Changed from SemVer to string to handle non-SemVer version strings (Change NodeInfo software.version field type from SemVer to string #366, Change NodeInfo software.version field type from SemVer to string #433)."per-inbox": Was "per-origin" in 1.x (Posting the same activity ID to different inboxes does not result in correct delivery #441).@fedify/fedify/x/* modules removed: Use dedicated @fedify/* packages instead (Remove deprecated @fedify/fedify/x/* modules in Fedify 2.0 #391).KvStore.list() is now required: Was optional since 1.10.0 (Make KvStore.list() required in 2.0.0 #499, Make KvStore.list() method required instead of optional #506).@fedify/fedify/vocab deprecated: Use @fedify/vocab instead (Extract vocab from @fedify/fedify into @fedify/vocab #437, Extract vocab from @fedify/fedify into @fedify/vocab #517).@fedify/fedify/runtime deprecated: Use @fedify/vocab-runtime instead (@fedify/vocab bundles its own LanguageString class, breaking instanceof checks #560).MessageQueue interface extended: All implementations must support the new orderingKey option (Activities can be delivered out of order, causing federation issues #536, Add orderingKey option to MessageQueueEnqueueOptions for ordered message delivery #538).Fedify 2.0.0 introduces a fundamental restructuring of the package architecture. What was previously a monolithic @fedify/fedify package with @fedify/fedify/vocab, @fedify/fedify/runtime, and @fedify/fedify/x/* submodules has been split into focused, independent packages:
The old import paths (@fedify/fedify/vocab and @fedify/fedify/runtime) still work as re-exports for backward compatibility, but they are deprecated and will be removed in a future version. The @fedify/fedify/x/* modules have been fully removed—you must migrate to the dedicated packages.
This modularization was primarily contributed by ChanHaeng Lee (@2chanhaeng).
@fedify/vocab: Activity Vocabulary packageThe generated Activity Vocabulary classes (e.g., Create, Note, Person, Follow) are now in the standalone @fedify/vocab package. This separation enables:
@fedify/vocab-tools package// Before (still works but deprecated):
import { Create, Note } from "@fedify/fedify/vocab"; // After:
import { Create, Note } from "@fedify/vocab";@fedify/vocab-runtime: Vocabulary runtime packageCore runtime utilities for vocabulary processing—DocumentLoader, LanguageString, cryptographic key utilities, and multibase encoding—have been extracted into @fedify/vocab-runtime:
// Before (still works but deprecated):
import { LanguageString } from "@fedify/fedify/runtime"; // After:
import { LanguageString } from "@fedify/vocab-runtime";Note that @fedify/vocab re-exports LanguageString, DocumentLoader, and RemoteDocument from @fedify/vocab-runtime, so downstream consumers typically do not need to depend on @fedify/vocab-runtime directly.
@fedify/vocab-tools: Custom vocabulary generationThe new @fedify/vocab-tools package provides the code generation infrastructure that Fedify itself uses to generate Activity Vocabulary classes. This enables you to extend ActivityPub with custom vocabulary types:
fedify generate-vocab CLI command@fedify/webfinger: Standalone WebFinger clientWebFinger functionality has been extracted into a standalone package for applications that need WebFinger lookup without the full Fedify framework:
import { lookupWebFinger } from "@fedify/webfinger"; const result = await lookupWebFinger("@user@example.com");@fedify/fresh: Fresh 2.0 integrationThe deprecated @fedify/fedify/x/fresh module (designed for Fresh 1.x) has been replaced by the new @fedify/fresh package with full Fresh 2.0 support:
import { integrateHandler } from "@fedify/fresh";
import { federation } from "./federation.ts"; export const handler = integrateHandler(federation, () => undefined);This was contributed by Hyeonseo Kim (@dodok8).
Fedify 2.0.0 introduces @fedify/debugger, an embedded real-time ActivityPub debug dashboard that provides unprecedented visibility into your federation traffic during development.
import { createFederation } from "@fedify/fedify";
import { createFederationDebugger } from "@fedify/debugger"; const innerFederation = createFederation({ /* ... */ });
const federation = createFederationDebugger(innerFederation);
// Use `federation` as a drop-in replacement—the dashboard is at /__debug__/That's it. createFederationDebugger() wraps your existing Federation object and automatically sets up OpenTelemetry tracing, span export, and LogTape integration—no manual configuration needed.
The debug dashboard, accessible at /__debug__/ by default, provides:
/__debug__/api/traces and /__debug__/api/logs/:traceIdProtect the dashboard in shared environments with built-in authentication:
const federation = createFederationDebugger(innerFederation, { auth: { password: "my-secret" }, // Or: auth: { username: "admin", password: "secret" } // Or: auth: (request) => request.headers.get("X-Forwarded-For") === "127.0.0.1"
});The debugger automatically captures LogTape log records grouped by trace ID. In the simplified setup (without explicit exporter), LogTape is auto-configured. For advanced setups, the returned object includes a sink property for manual LogTape configuration.
To support this, Fedify now injects traceId and spanId into the LogTape context during request handling and queue processing, enabling log correlation with OpenTelemetry traces (#561, #564).
Fedify 2.0.0 introduces first-class ActivityPub relay support through the new @fedify/relay package and the fedify relay CLI command.
@fedify/relay packageActivityPub relays are critical fediverse infrastructure that help smaller instances participate in content distribution. The new @fedify/relay package provides a ready-to-use relay server implementation:
import { createRelay } from "@fedify/relay";
import { MemoryKvStore } from "@fedify/fedify"; const relay = createRelay("mastodon", { kv: new MemoryKvStore(), origin: new URL("https://relay.example.com"), subscriptionHandler: async (ctx, subscriber) => { // Approve or reject subscriptions return "accepted"; },
}); // Use relay.fetch() to handle incoming requestsThe package supports two relay protocols as defined in FEP-ae0c:
"mastodon"): Direct activity forwarding with one-way following and immediate subscription acceptance. Forwards Create, Update, Delete, Move, and Announce activities. Broader fediverse compatibility."litepub"): Activities wrapped in Announce, bidirectional following with pending-then-accepted state. Designed for LitePub-aware servers.Relay management features include subscriber listing (relay.listFollowers()), individual subscriber lookup (relay.getFollower()), and automatic signature verification.
fedify relay CLI commandFor quick testing and development, the new fedify relay command spins up an ephemeral relay server:
# Start a Mastodon-compatible relay with public tunnel
fedify relay # LitePub relay with persistent storage
fedify relay --protocol litepub --persistent ./relay.db # Accept only specific instances
fedify relay --accept-follow "mastodon.social,hachyderm.io" # Reject specific instances
fedify relay --reject-follow "spam.example.com"By default, the relay server is tunneled to the public internet for external access. Use --no-tunnel to run locally only.
This feature was primarily contributed by Jiwon Kwon (@sij411).
One of the most impactful changes in Fedify 2.0.0 is the introduction of ordering keys for message queues, solving the long-standing “zombie post” problem in ActivityPub federation (#536).
When a post is created and then quickly deleted, the Delete activity can arrive at remote instances before the Create activity due to parallel message processing. This results in “zombie posts”—content that should have been deleted but persists because the delete was processed before the create.
The new orderingKey option in MessageQueueEnqueueOptions guarantees FIFO processing for messages sharing the same key, while allowing messages with different keys to be processed in parallel:
// Activities for the same note are delivered in order
await ctx.sendActivity(sender, recipients, createNote, { orderingKey: noteId,
});
await ctx.sendActivity(sender, recipients, deleteNote, { orderingKey: noteId,
});When orderingKey is specified in SendActivityOptions, the key is automatically transformed to ${orderingKey}\n${recipientServerOrigin} during fan-out, ensuring per-recipient-server ordering while maintaining cross-server parallelism.
All official MessageQueue implementations have been updated:
| Backend | Ordering mechanism |
|---|---|
InProcessMessageQueue |
Built-in FIFO per key |
PostgresMessageQueue |
SELECT FOR UPDATE SKIP LOCKED |
SqliteMessageQueue |
Row-level ordering |
RedisMessageQueue |
Redis Streams |
AmqpMessageQueue |
rabbitmq_consistent_hash_exchange plugin |
WorkersMessageQueue |
Workers KV locks (best-effort) |
Note for custom implementations: If you have a custom
MessageQueueimplementation, you must add support for theorderingKeyoption in theenqueue()method. Messages with the sameorderingKeymust be processed in FIFO order.
Fedify 2.0.0 introduces a mechanism for handling permanent delivery failures when sending activities to remote inboxes (#548, #559).
Previously, when a remote inbox returned 410 Gone or 404 Not Found, Fedify treated it as a transient failure and continued retrying. This wasted resources and provided no way for applications to clean up unreachable followers.
setOutboxPermanentFailureHandler()The new setOutboxPermanentFailureHandler() method lets you react to permanent failures:
federation.setOutboxPermanentFailureHandler(async (ctx, values) => { const { inbox, activity, error, statusCode, actorIds } = values; // Clean up followers pointing to the failed inbox for (const actorId of actorIds) { await removeFollower(actorId, inbox); }
});The handler receives:
inbox: The failing inbox URLactivity: The Activity object that failed to delivererror: A SendActivityError instance with HTTP status code and response detailsstatusCode: The HTTP status codeactorIds: Actor IDs intended to receive the activity at this inbox (relevant for shared inbox delivery)By default, HTTP status codes 404 and 410 are treated as permanent failures. Customize this via permanentFailureStatusCodes:
const federation = createFederation({ // ... permanentFailureStatusCodes: [404, 410, 451], // Add 451 Unavailable For Legal Reasons
});SendActivityErrorThe new SendActivityError class provides structured error information for delivery failures, including the HTTP status code, inbox URL, and response body (limited to 1 KiB to prevent memory pressure from large error pages) (#569).
Fedify 2.0.0 moves content type negotiation from individual dispatchers to the middleware layer (#434, contributed by Emelia Smith). This is a breaking change that improves compatibility with applications serving both HTML and ActivityPub content from the same URLs.
Previously, actor, object, and collection dispatchers were called for all incoming requests, regardless of the Accept header. Applications had to handle content negotiation within dispatchers. Now, dispatchers are only invoked when the request accepts ActivityPub-compatible content types (application/activity+json, application/ld+json, etc.).
Accept: text/html (e.g., browser requests) no longer reach your dispatchers—they are passed through to your web frameworkonNotAcceptable callback is triggered at the middleware level before dispatchers are invokedThis change simplifies the common pattern of serving both a web page and an ActivityPub representation at the same URL, as the framework now handles the routing decision automatically.
"per-inbox"The default activity idempotency strategy has changed from "per-origin" to "per-inbox" to align with standard ActivityPub behavior (#441).
In Fedify 1.x, activity deduplication was per-origin by default—the same activity ID would be processed only once per receiving server, regardless of how many inboxes on that server it was delivered to. This caused issues when:
With "per-inbox" as the new default, each inbox independently tracks which activity IDs it has seen. The same activity can be processed once per inbox, which is the standard ActivityPub behavior.
To preserve the old behavior:
federation .setInboxListeners("/inbox/{identifier}", "/inbox") .withIdempotency("per-origin") // Explicitly set old behavior .on(Follow, async (ctx, follow) => { // ... });KvStore.list() is now requiredThe list() method on the KvStore interface, introduced as optional in Fedify 1.10.0, is now required in 2.0.0 (#499, #506).
interface KvStore { get(key: KvKey): Promise<unknown>; set(key: KvKey, value: unknown, options?: KvStoreSetOptions): Promise<void>; delete(key: KvKey): Promise<void>; // Now required: list(prefix?: KvKey): AsyncIterable<KvStoreListEntry>;
}All official KvStore implementations already support this method: MemoryKvStore, SqliteKvStore, PostgresKvStore, RedisKvStore, DenoKvStore, and WorkersKvStore.
If you have a custom KvStore implementation, you must add a list() method that enumerates all entries whose keys start with the given prefix.
@fedify/lint packageThe new @fedify/lint package provides shared linting configurations for consistent code quality in Fedify-based projects (#297, #494, contributed by ChanHaeng Lee).
It supports both Deno Lint and ESLint, with 18 lint rules covering:
Two presets are available: recommended (default) and strict.
@fedify/create and @fedify/init packagesCreating new Fedify projects is now easier than ever with the new @fedify/create package (#351, contributed by ChanHaeng Lee):
npm init @fedify # npm
pnpm create @fedify # pnpm
yarn create @fedify # yarn
bunx @fedify/create # BunThis provides the familiar npm init workflow that JavaScript developers expect, without needing to install the full @fedify/cli toolchain. The core initialization logic lives in the @fedify/init package, which is shared by both @fedify/create and the fedify init CLI command.
SqliteMessageQueueThe @fedify/sqlite package now includes SqliteMessageQueue, a MessageQueue implementation using SQLite as the backing store (#477, #526, contributed by ChanHaeng Lee). This is ideal for development environments and small-scale, single-node production deployments:
import { SqliteMessageQueue } from "@fedify/sqlite"; const queue = new SqliteMessageQueue("./queue.db");
await queue.initialize();SqliteMessageQueue supports the new orderingKey option for ordered message delivery.
The Fedify CLI now runs natively on Node.js and Bun without requiring compiled binaries, providing a more natural JavaScript package experience (#374, #456, #457).
fedify generate-vocab commandGenerate Activity Vocabulary classes from schema files using the new fedify generate-vocab command. This uses @fedify/vocab-tools internally and enables extending ActivityPub with custom vocabulary types (#444, #458, contributed by ChanHaeng Lee).
fedify initThe fedify init command has been improved with better DX (#397, #435, contributed by ChanHaeng Lee):
fedify lookup --traverseThe fedify lookup command now supports traversing multiple collections in a single command with the -t/--traverse option (#408, #449, contributed by Jiwon Kwon).
--tunnel-service optionThe fedify lookup, fedify inbox, and fedify relay commands now support a --tunnel-service option to select the tunneling service (localhost.run, serveo.net, or pinggy.io) (#525, #529, #531, contributed by Jiwon Kwon).
The CLI now loads settings from TOML configuration files at multiple levels (#555, #566, contributed by Jiwon Kwon):
--config optionAll command options (inbox, lookup, webfinger, nodeinfo, tunnel, relay) can be configured through these files. Use --ignore-config to skip configuration file loading.
Intl.Locale replaces LanguageTagThe @phensley/language-tag dependency has been replaced with the standardized Intl.Locale class (#280, #392, contributed by Jang Hanarae):
// Before:
const lang: LanguageTag = langString.language; // After:
const locale: Intl.Locale = langString.locale;stringNodeInfo software.version is now string instead of SemVer to properly handle non-SemVer version strings in accordance with the NodeInfo specification (#366, #433, contributed by Hyeonseo Kim). The parseSemVer() and formatSemVer() functions have been removed.
KvCacheParameters.rules type relaxedThe rules option type now accepts Temporal.DurationLike in addition to Temporal.Duration, making it easier to specify cache durations:
// Before: had to use Temporal.Duration.from()
rules: [[new URL("https://example.com"), Temporal.Duration.from({ hours: 1 })]], // After: plain objects work too
rules: [[new URL("https://example.com"), { hours: 1 }]],The @fedify/testing package now includes testMessageQueue(), a reusable test harness for standardized testing of MessageQueue implementations (#477, #526, contributed by ChanHaeng Lee). It covers common operations including enqueue(), enqueue() with delay, enqueueMany(), multiple listeners, and (optionally) ordering key tests.
The @fedify/elysia package now includes a deno.json configuration file for proper Deno tooling support (#460, #496).
Replace deprecated import paths with new packages:
// Vocabulary types
// Before:
import { Create, Note, Person } from "@fedify/fedify/vocab";
// After:
import { Create, Note, Person } from "@fedify/vocab"; // Runtime utilities
// Before:
import { LanguageString } from "@fedify/fedify/runtime";
// After:
import { LanguageString } from "@fedify/vocab-runtime"; // Framework integrations
// Before:
import { federation } from "@fedify/fedify/x/hono";
// After:
import { federation } from "@fedify/hono";// Before:
const federation = createFederation({ documentLoader: myLoader, // Removed contextLoader: myContextLoader, // Removed
}); // After:
const federation = createFederation({ documentLoaderFactory: (handle) => myLoader, contextLoaderFactory: (handle) => myContextLoader,
}); // Before:
import { fetchDocumentLoader } from "@fedify/fedify/runtime";
// After:
import { getDocumentLoader } from "@fedify/vocab-runtime"; // Before:
ctx.sendActivity({ handle: "alice" }, recipients, activity);
// After:
ctx.sendActivity({ identifier: "alice" }, recipients, activity);LanguageTag usage// Before:
import { LanguageTag } from "@phensley/language-tag";
const lang: LanguageTag = langString.language; // After:
const locale: Intl.Locale = langString.locale;
// Or construct from string:
const locale = new Intl.Locale("en-US");// Before:
import { parseSemVer } from "@fedify/fedify";
const version: SemVer = software.version; // After:
const version: string = software.version;
// Parse yourself if needed: const parts = version.split(".");Dispatchers now only fire for requests with ActivityPub-compatible Accept headers. If your dispatcher contained logic for non-ActivityPub requests (e.g., rendering HTML or logging all visits), that code will no longer execute for browser requests:
// Before (1.x): Dispatcher was called for ALL requests, including browsers.
// Some apps relied on this for side effects or manual content negotiation:
federation.setActorDispatcher("/users/{identifier}", async (ctx, identifier) => { // This code ran even for Accept: text/html requests in 1.x. // In 2.0, this is ONLY called for ActivityPub content types. return new Person({ /* ... */ });
}); // After (2.0): If you need to handle both HTML and ActivityPub at the same URL,
// rely on the onNotAcceptable callback in your middleware integration:
return await federation.fetch(request, { contextData, onNotFound: async (request) => await next(request), onNotAcceptable: async (request) => { // Fedify calls this when the route matches but Accept is not ActivityPub. // Forward to your web framework to render HTML: const response = await next(request); if (response.status !== 404) return response; return new Response("Not Acceptable", { status: 406, headers: { "Content-Type": "text/plain", Vary: "Accept" }, }); },
});// To keep the old 1.x behavior:
federation .setInboxListeners("/inbox/{identifier}", "/inbox") .withIdempotency("per-origin"); // Or accept the new default (recommended):
// "per-inbox" is now the default—no code change neededKvStore.list() (custom implementations only)If you have a custom KvStore implementation, add the list() method:
class MyKvStore implements KvStore { // ... existing methods ... async *list(prefix?: KvKey): AsyncIterable<KvStoreListEntry> { // Enumerate entries matching the prefix for (const [key, value] of this.entries()) { if (!prefix || keyStartsWith(key, prefix)) { yield { key, value }; } } }
}Fedify 2.0.0 represents an extraordinary collaborative effort. Special thanks to:
@fedify/vocab, @fedify/vocab-runtime, @fedify/vocab-tools, @fedify/init, @fedify/create, @fedify/lint, @fedify/fedify/x/* separation@fedify/relay, fedify relay command, fedify lookup --traverse, --tunnel-service option, CLI configuration files, Redis race condition fix@fedify/fresh (Fresh 2.0), Elysia framework support, NodeInfo version handlingcontextLoader/documentLoader factory migrationIntl.Locale migrationAnd to all community members who reported issues, provided feedback, and tested pre-release versions.
For the complete list of changes, bug fixes, and improvements, please refer to the CHANGES.md file in the repository.
Fedify is really fun to mess around with. The fedify tutorial was also really great for learning about developing with Activity pub and the fediverse in general.
I don't use Discord generally, but the fedify Discord is particularly useful, and I see how some discussions there have evolved into features in this release which is nice too!
Awesome. Contributing to relay was fun but testing with actual Mastodon and others was hard >_<
[flagged]