The fedify command is now available on #npm! You can install it using the following command:
npm install -g @fedify/cli
Or if you use #Bun:
bun install -g @fedify/cli
The fedify command is now available on #npm! You can install it using the following command:
npm install -g @fedify/cli
Or if you use #Bun:
bun install -g @fedify/cli
So, you're captivated by the fediverse—the decentralized social web powered by protocols like ActivityPub. Maybe you're dreaming of building the next great federated app, a unique space connected to Mastodon, Lemmy, Pixelfed, and more. The temptation to dive deep and implement ActivityPub yourself, from the ground up, is strong. Total control, right? Understanding every byte? Sounds cool!
But hold on a sec. Before you embark on that epic quest, let's talk reality. Implementing ActivityPub correctly isn't just one task; it's like juggling several complex standards while riding a unicycle… blindfolded. It’s hard.
That's where Fedify comes in. It's a TypeScript framework designed to handle the gnarliest parts of ActivityPub development, letting you focus on what makes your app special, not reinventing the federation wheel.
This post will break down the common headaches of DIY ActivityPub implementation and show how Fedify acts as the super-powered pain reliever, starting with the very foundation of how data is represented.
Challenge #1: Data Modeling—Speaking ActivityStreams & JSON-LD Fluently
At its core, ActivityPub relies on the ActivityStreams 2.0 vocabulary to describe actions and objects, and it uses JSON-LD as the syntax to encode this vocabulary. While powerful, this combination introduces significant complexity right from the start.
First, understanding and correctly using the vast ActivityStreams vocabulary itself is a hurdle. You need to model everything—posts (Note, Article), profiles (Person, Organization), actions (Create, Follow, Like, Announce)—using the precise terms and properties defined in the specification. Manual JSON construction is tedious and prone to errors.
Second, JSON-LD, the encoding layer, has specific rules that make direct JSON manipulation surprisingly tricky:
Missing vs. Empty Array: In JSON-LD, a property being absent is often semantically identical to it being present with an empty array. Your application logic needs to treat these cases equally when checking for values. For example, these two Note objects mean the same thing regarding the name property:
// No name property{ "@context": "https://www.w3.org/ns/activitystreams", "type": "Note", "content": "…"}
// Equivalent to:{ "@context": "https://www.w3.org/ns/activitystreams", "type": "Note", "name": [], "content": "…"}
// Single value{ "@context": "https://www.w3.org/ns/activitystreams", "type": "Note", "content": "Hello"}
// Equivalent to:{ "@context": "https://www.w3.org/ns/activitystreams", "type": "Note", "content": ["Hello"]}
{ "@context": "https://www.w3.org/ns/activitystreams", "type": "Announce", // Embedded objects: "actor": { "type": "Person", "id": "http://sally.example.org/", "name": "Sally" }, "object": { "type": "Arrive", "id": "https://sally.example.com/arrive", /* ... */ }}
// Equivalent to:{ "@context": "https://www.w3.org/ns/activitystreams", "type": "Announce", // URI references: "actor": "http://sally.example.org/", "object": "https://sally.example.com/arrive"}
Attempting to manually handle all these vocabulary rules and JSON-LD variations consistently across your application inevitably leads to verbose, complex, and fragile code, ripe for subtle bugs that break federation.
Fedify tackles this entire data modeling challenge with its comprehensive, type-safe Activity Vocabulary API. It provides TypeScript classes for ActivityStreams types and common extensions, giving you autocompletion and compile-time safety. Crucially, these classes internally manage all the tricky JSON-LD nuances. Fedify's property accessors present a consistent interface—non-functional properties (like tags) always return arrays, functional properties (like content) always return single values or null. It handles object references versus embedded objects seamlessly through dereferencing accessors (like activity.getActor()) which automatically fetch remote objects via URI when needed—a feature known as property hydration. With Fedify, you work with a clean, predictable TypeScript API, letting the framework handle the messy details of AS vocabulary and JSON-LD encoding.
Challenge #2: Discovery & Identity—Finding Your Actors
Once you can model data, you need to make your actors discoverable. This primarily involves the WebFinger protocol (RFC 7033). You'd need to build a server endpoint at /.well-known/webfinger capable of parsing resource queries (like acct: URIs), validating the requested domain against your server, and responding with a precisely formatted JSON Resource Descriptor (JRD). This JRD must include specific links, like a self link pointing to the actor's ActivityPub ID using the correct media type. Getting any part of this wrong can make your actors invisible.
Fedify simplifies this significantly. It automatically handles WebFinger requests based on the actor information you provide through its setActorDispatcher() method. Fedify generates the correct JRD response. If you need more advanced control, like mapping user-facing handles to internal identifiers, you can easily register mapHandle() or mapAlias() callbacks. You focus on defining your actors; Fedify handles making them discoverable.
// Example: Define how to find actorsfederation.setActorDispatcher( "/users/{username}", async (ctx, username) => { /* ... */ });// Now GET /.well-known/webfinger?resource=acct:username@your.domain just works!
Challenge #3: Core ActivityPub Mechanics—Handling Requests and Collections
Serving actor profiles requires careful content negotiation. A request for an actor's ID needs JSON-LD for machine clients (Accept: application/activity+json) but HTML for browsers (Accept: text/html). Handling incoming activities at the inbox endpoint involves validating POST requests, verifying cryptographic signatures, parsing the payload, preventing duplicates (idempotency), and routing based on activity type. Implementing collections (outbox, followers, etc.) with correct pagination adds another layer.
Fedify streamlines all of this. Its core request handler (via Federation.fetch() or framework adapters like @fedify/express) manages content negotiation. You define actors with setActorDispatcher() and web pages with your framework (Hono, Express, SvelteKit, etc.)—Fedify routes appropriately. For the inbox, setInboxListeners() lets you define handlers per activity type (e.g., .on(Follow, ...)), while Fedify automatically handles validation, signature verification, parsing, and idempotency checks using its KV Store. Collection implementation is simplified via dispatchers (e.g., setFollowersDispatcher()); you provide logic to fetch a page of data, and Fedify constructs the correct Collection or CollectionPage with pagination.
// Define inbox handlersfederation.setInboxListeners("/inbox", "/users/{handle}/inbox") .on(Follow, async (ctx, follow) => { /* Handle follow */ }) .on(Undo, async (ctx, undo) => { /* Handle undo */ });// Define followers collection logicfederation.setFollowersDispatcher( "/users/{handle}/followers", async (ctx, handle, cursor) => { /* ... */ });
Challenge #4: Reliable Delivery & Asynchronous Processing—Sending Activities Robustly
Sending an activity requires more than a simple POST. Networks fail, servers go down. You need robust failure handling and retry logic (ideally with backoff). Processing incoming activities synchronously can block your server. Efficiently broadcasting to many followers (fan-out) requires background processing and using shared inboxes where possible.
Fedify addresses reliability and scalability using its MessageQueue abstraction. When configured (highly recommended), Context.sendActivity() enqueues delivery tasks. Background workers handle sending with automatic retries based on configurable policies (like outboxRetryPolicy). Fedify supports various queue backends (Deno KV, Redis, PostgreSQL, AMQP). For high-traffic fan-out, Fedify uses an optimized two-stage mechanism to distribute the load efficiently.
// Configure Fedify with a persistent queue (e.g., Deno KV)const federation = createFederation({ queue: new DenoKvMessageQueue(/* ... */), // ...});// Sending is now reliable and non-blockingawait ctx.sendActivity({ handle: "myUser" }, recipient, someActivity);
Challenge #5: Security—Avoiding Common Pitfalls
Securing an ActivityPub server is critical. You need to implement HTTP Signatures (draft-cavage-http-signatures-12) for server-to-server authentication—a complex process. You might also need Linked Data Signatures (LDS) or Object Integrity Proofs (OIP) based on FEP-8b32 for data integrity and compatibility. Managing cryptographic keys securely is essential. Lastly, fetching remote resources risks Server-Side Request Forgery (SSRF) if not validated properly.
Fedify is designed with security in mind. It automatically handles the creation and verification of HTTP Signatures, LDS, and OIP, provided you supply keys via setKeyPairsDispatcher. It includes key management utilities. Crucially, Fedify's default document loader includes built-in SSRF protection, blocking requests to private IPs unless explicitly allowed.
Challenge #6: Interoperability & Maintenance—Playing Nicely with Others
The fediverse is diverse. Different servers have quirks. Ensuring compatibility requires testing and adaptation. Standards evolve with new Federation Enhancement Proposals (FEPs). You also need protocols like NodeInfo to advertise server capabilities.
Fedify aims for broad interoperability and is actively maintained. It includes features like ActivityTransformers to smooth over implementation differences. NodeInfo support is built-in via setNodeInfoDispatcher.
Challenge #7: Developer Experience—Actually Building Your App
Beyond the protocol, building any server involves setup, testing, and debugging. With federation, debugging becomes harder—was the message malformed? Was the signature wrong? Is the remote server down? Is it a compatibility quirk? Good tooling is essential.
Fedify enhances the developer experience significantly. Being built with TypeScript, it offers excellent type safety and editor auto-completion. The fedify CLI is a powerful companion designed to streamline common development tasks.
You can quickly scaffold a new project tailored to your chosen runtime and web framework using fedify init.
For debugging interactions and verifying data, fedify lookup is invaluable. It lets you inspect how any remote actor or object appears from the outside by performing WebFinger discovery and fetching the object's data. Fedify then displays the parsed object structure and properties directly in your terminal. For example, running:
$ fedify lookup @fedify-example@fedify-blog.deno.dev
Will first show progress messages and then output the structured representation of the actor, similar to this:
// Output of fedify lookup command (shows parsed object structure)Person { id: URL "https://fedify-blog.deno.dev/users/fedify-example", name: "Fedify Example Blog", published: 2024-03-03T13:18:11.857Z, // Simplified timestamp summary: "This blog is powered by Fedify, a fediverse server framework.", url: URL "https://fedify-blog.deno.dev/", preferredUsername: "fedify-example", publicKey: CryptographicKey { id: URL "https://fedify-blog.deno.dev/users/fedify-example#main-key", owner: URL "https://fedify-blog.deno.dev/users/fedify-example", publicKey: CryptoKey { /* ... CryptoKey details ... */ } }, // ... other properties like inbox, outbox, followers, endpoints ...}
This allows you to easily check how data is structured or troubleshoot why an interaction might be failing by seeing the actual properties Fedify parsed.
Testing outgoing activities from your application during development is made much easier with fedify inbox. Running the command starts a temporary local server that acts as a publicly accessible inbox, displaying key information about the temporary actor it creates for receiving messages:
$ fedify inbox✔ The ephemeral ActivityPub server is up and running: https://<unique_id>.lhr.life/✔ Sent follow request to @<some_test_account>@activitypub.academy.╭───────────────┬─────────────────────────────────────────╮│ Actor handle: │ i@<unique_id>.lhr.life │├───────────────┼─────────────────────────────────────────┤│ Actor URI: │ https://<unique_id>.lhr.life/i │├───────────────┼─────────────────────────────────────────┤│ Actor inbox: │ https://<unique_id>.lhr.life/i/inbox │├───────────────┼─────────────────────────────────────────┤│ Shared inbox: │ https://<unique_id>.lhr.life/inbox │╰───────────────┴─────────────────────────────────────────╯Web interface available at: http://localhost:8000/
You then configure your developing application to send an activity to the Actor inbox or Shared inbox URI provided. When an activity arrives, fedify inbox only prints a summary table to your console indicating that a request was received:
╭────────────────┬─────────────────────────────────────╮│ Request #: │ 2 │├────────────────┼─────────────────────────────────────┤│ Activity type: │ Follow │├────────────────┼─────────────────────────────────────┤│ HTTP request: │ POST /i/inbox │├────────────────┼─────────────────────────────────────┤│ HTTP response: │ 202 │├────────────────┼─────────────────────────────────────┤│ Details │ https://<unique_id>.lhr.life/r/2 │╰────────────────┴─────────────────────────────────────╯
Crucially, the detailed information about the received request—including the full headers (like Signature), the request body (the Activity JSON), and the signature verification status—is only available in the web interface provided by fedify inbox. This web UI allows you to thoroughly inspect incoming activities during development.
The Fedify Inbox web UI is where you view detailed activity information.When you need to test interactions with the live fediverse from your local machine beyond just sending, fedify tunnel can securely expose your entire local development server temporarily. This suite of tools significantly eases the process of building and debugging federated applications.
Conclusion: Build Features, Not Plumbing
Implementing the ActivityPub suite of protocols from scratch is an incredibly complex and time-consuming undertaking. It involves deep dives into multiple technical specifications, cryptographic signing, security hardening, and navigating the nuances of a diverse ecosystem. While educational, it dramatically slows down the process of building the actual, unique features of your federated application.
Fedify offers a well-architected, secure, and type-safe foundation, handling the intricacies of federation for you—data modeling, discovery, core mechanics, delivery, security, and interoperability. It lets you focus on your application's unique value and user experience. Stop wrestling with low-level protocol details and start building your vision for the fediverse faster and more reliably. Give Fedify a try!
Getting started is straightforward. First, install the Fedify CLI using your preferred method. Once installed, create a new project template by running fedify init your-project-name.
Check out the Fedify tutorials and Fedify manual to learn more. Happy federating!
Takahe has limited support for this type: See Original Article#Hollo is currently testing #Node.js instead of #Bun. (In fact, the hollo.social server is already running on Node.js!) If this test is successful, starting with the next release, Hollo will be powered by Node.js instead of Bun.
The main reason for switching to Node.js is to optimize memory usage. As you can see in the graph image below, Node.js uses significantly less memory than Bun. With this switch, Hollo is expected to be even more lightweight than before!
Are you interested in trying out the Node.js version of Hollo early? Try to pull ghcr.io/dahlia/hollo:0.4.0-dev.290!
한국어 (Korean): LogTape 0.12.0 릴리스 노트
日本語 (Japanese): LogTape 0.12.0 リリースノート
中文(台灣) (Chinese (Taiwan)): LogTape 0.12.0 發布說明
中文(中国) (Chinese (China)): LogTape 0.12.0 发布说明
LogTape is a zero-dependency logging library for JavaScript and TypeScript that provides a simple yet flexible logging system. It supports multiple JavaScript runtimes (Deno, Node.js, Bun, browsers, and edge functions), features hierarchical categories, structured logging, and offers seamless integration for both applications and libraries.
What's New in 0.12.0
Trace Log Level
LogTape now includes a trace severity level, which sits below debug in the verbosity hierarchy. This addition provides finer-grained control over logging output, particularly useful for detailed execution flow tracking during development and debugging.
Added "trace" to the LogLevel union type
Added Logger.trace() method for logging trace-level messages
The complete severity hierarchy is now: trace < debug < info < warning < error < fatal
Enhanced File Sink Performance
File sinks now support configurable buffering, significantly improving write performance for high-volume logging scenarios.
Added bufferSize option (default: 8192 characters) to control write buffering behavior
Added flushInterval option (default: 5000ms) for automatic time-based buffer flushing
Set bufferSize to 0 for immediate writes without buffering
Set flushInterval to 0 to disable time-based flushing
Buffer contents are automatically flushed when the sink is disposed
These options are available for both getFileSink() and getRotatingFileSink() functions.
Syslog Support
The new @logtape/syslog package enables sending log messages to syslog servers using the RFC 5424 format.
Support for both UDP and TCP protocols
All standard RFC 5424 facilities (kern, user, mail, daemon, local0–7, etc.)
Automatic priority calculation based on log levels
Structured data support for log record properties
Cross-runtime compatibility with Deno, Node.js, and Bun
Configurable connection timeouts, custom hostnames, and application names
Logger Method Alias
Added Logger.warning() as an alias for Logger.warn() to ensure consistency with the LogLevel type definition. This change addresses the naming mismatch where the LogLevel union type uses "warning" while the logger method was named warn(), making metaprogramming and dynamic method invocation more straightforward.
Unified Package Releases
Starting with version 0.12.0, all LogTape packages including @logtape/otel, @logtape/sentry, and @logtape/syslog share the same version number and are released together. This ensures compatibility between packages and simplifies version management for users.
Improved Build Infrastructure
LogTape has migrated from dnt to tsdown for npm package bundling. tsdown is a library-focused bundler built on top of Rolldown, a Rust-based bundler that powers the next generation of Vite. Unlike general-purpose bundlers, tsdown is specifically optimized for building TypeScript and JavaScript libraries with minimal configuration. This change brings several benefits:
Elimination of bundler warnings in Webpack, Vite, and other build tools
Improved compatibility with modern JavaScript toolchains
Better tree-shaking support
Cleaner package output
Faster build times through Rust-based performance optimizations
Migration Guide
Updating to Trace Level
If you have code that relies on debug being the lowest severity level, you may need to update your log level configurations:
// Before{ lowestLevel: "debug" } // This was the most verbose setting// After{ lowestLevel: "trace" } // Now includes trace messages
Leveraging Buffer Configuration
To optimize file sink performance in high-throughput scenarios:
getFileSink("app.log", { bufferSize: 16384, // Larger buffer for better performance flushInterval: 10_000 // Flush every 10 seconds})
Installation
LogTape 0.12.0 is available on JSR and npm:
deno add jsr:@logtape/logtape # Denonpm add @logtape/logtape # npmpnpm add @logtape/logtape # pnpmyarn add @logtape/logtape # yarnbun add @logtape/logtape # Bun
For the syslog sink:
deno add jsr:@logtape/syslog # Denonpm add @logtape/syslog # npmpnpm add @logtape/syslog # pnpmyarn add @logtape/syslog # yarnbun add @logtape/syslog # Bun
Acknowledgments
We thank all contributors who helped make this release possible, including those who reported issues, submitted pull requests, and provided feedback.
For the complete list of changes, please refer to the changelog.
Takahe has limited support for this type: See Original Article한국어 (Korean): LogTape 1.0.0 출시 발표
日本語 (Japanese): LogTape 1.0.0 発表のお知らせ
What is LogTape?
LogTape is a logging library designed specifically for the modern JavaScript ecosystem. It stands out with its zero-dependency architecture, universal runtime support across Node.js, Deno, Bun, browsers, and edge functions, and a library-first design philosophy that allows library authors to add logging without imposing any burden on their users. When LogTape isn't configured, logging calls have virtually no performance impact, making it the only truly unobtrusive logging solution available.
For a comprehensive overview of LogTape's capabilities and philosophy, see our introduction guide.
Milestone achievement
We're excited to announce LogTape 1.0.0, marking a significant milestone in the library's development. This release represents our commitment to API stability and long-term support. The 1.0.0 designation signals that LogTape's core APIs are now stable and ready for production use, with any future breaking changes following semantic versioning principles.
This milestone builds upon months of refinement, community feedback, and real-world usage, establishing LogTape as a mature and reliable logging solution for JavaScript applications and libraries.
Major new features
High-performance logging infrastructure
LogTape 1.0.0 introduces several performance-oriented features designed for high-throughput production environments. The new non-blocking sink option allows console, stream, and file sinks to buffer log records and flush them asynchronously, preventing logging operations from blocking your application's main thread.
import { configure, getConsoleSink } from "@logtape/logtape";await configure({ sinks: { console: getConsoleSink({ nonBlocking: { bufferSize: 1000, flushInterval: 50 } }) }, // ...});
The new fromAsyncSink() function provides a clean way to integrate asynchronous logging operations while maintaining LogTape's synchronous sink interface. This enables scenarios like sending logs to remote servers or databases without blocking your application.
import { fromAsyncSink } from "@logtape/logtape";const webhookSink = fromAsyncSink(async (record) => { await fetch("https://logs.example.com", { method: "POST", body: JSON.stringify(record) });});
For file operations specifically, the new getStreamFileSink() function in the @logtape/file package leverages Node.js PassThrough streams to deliver optimal I/O performance with automatic backpressure management.
New sink integrations
This release significantly expands LogTape's integration capabilities with two major new sink packages. The @logtape/cloudwatch-logs package enables direct integration with AWS CloudWatch Logs, featuring intelligent batching, exponential backoff retry strategies, and support for structured logging through JSON Lines formatting.
import { getCloudWatchLogsSink } from "@logtape/cloudwatch-logs";const sink = getCloudWatchLogsSink({ logGroupName: "/aws/lambda/my-function", logStreamName: "my-stream", region: "us-east-1"});
The @logtape/windows-eventlog package brings native Windows Event Log support with cross-runtime compatibility across Deno, Node.js, and Bun. This integration uses runtime-optimized FFI implementations for maximum performance while maintaining proper error handling and resource cleanup.
Beautiful development experience
The new @logtape/pretty package transforms console logging into a visually appealing experience designed specifically for local development. Inspired by Signale, it features colorful emojis for each log level, smart category truncation that preserves important context, and perfect column alignment that makes logs easy to scan.
import { configure, getConsoleSink } from "@logtape/logtape";import { prettyFormatter } from "@logtape/pretty";await configure({ sinks: { console: getConsoleSink({ formatter: prettyFormatter }) }, // ...});
As shown above, the pretty formatter supports true color terminals with rich color schemes, configurable icons, and intelligent word wrapping that maintains visual consistency even for long messages.
Ecosystem integration
Perhaps most significantly, LogTape 1.0.0 introduces adapter packages that bridge the gap between LogTape's library-friendly design and existing logging infrastructure. The @logtape/adaptor-winston and @logtape/adaptor-pino packages allow applications using these established logging libraries to seamlessly integrate LogTape-enabled libraries without changing their existing setup.
// Quick setup with winstonimport "@logtape/adaptor-winston/install";
// Or with custom configurationimport { install } from "@logtape/adaptor-winston";import winston from "winston";const logger = winston.createLogger({/* your config */});install(logger);
These adapters preserve LogTape's structured logging capabilities while routing everything through your preferred logging system, making adoption of LogTape-enabled libraries frictionless for existing applications.
Developer experience enhancements
This release includes several quality-of-life improvements for developers working with LogTape. The new getLogLevels() function provides programmatic access to all available log levels, while the LogMethod type offers better type inference for logging methods.
Browser compatibility has been improved, particularly for the @logtape/otel package, which previously had issues in browser environments due to Node.js-specific imports. The package now works seamlessly across all JavaScript runtimes without throwing module resolution errors.
Breaking changes and migration guide
LogTape 1.0.0 includes one notable breaking change: the removal of the deprecated LoggerConfig.level property. This property was deprecated in version 0.8.0 in favor of the more descriptive LoggerConfig.lowestLevel property.
If your configuration still uses the old property, simply rename it:
// Before (deprecated){ category: ["app"], level: "info", sinks: ["console"] }
// After{ category: ["app"], lowestLevel: "info", sinks: ["console"] }
For more complex filtering requirements, consider using the LoggerConfig.filters option instead, which provides more flexibility and supports inheritance from parent loggers.
Complete package ecosystem
LogTape 1.0.0 represents the culmination of a comprehensive package ecosystem, now consisting of 11 specialized packages that address different aspects of logging infrastructure. This modular approach allows you to install only the packages you need, keeping your dependency footprint minimal while accessing powerful logging capabilities when required.
PackageJSRnpmDescription@logtape/logtapeJSRnpmCore logging functionality@logtape/adaptor-pinoJSRnpmPino adapter@logtape/adaptor-winstonJSRnpmwinston adapter@logtape/cloudwatch-logsJSRnpmAWS CloudWatch Logs sink@logtape/fileJSRnpmFile sinks@logtape/otelJSRnpmOpenTelemetry sink@logtape/prettyJSRnpmBeautiful text formatter@logtape/redactionJSRnpmData redaction@logtape/sentryJSRnpmSentry sink@logtape/syslogJSRnpmSyslog sink@logtape/windows-eventlogJSRnpmWindows Event Log sinkGetting started
Whether you're new to LogTape or upgrading from a previous version, getting started with 1.0.0 is straightforward. For new projects, begin with a simple configuration and gradually add the packages and features you need:
import { configure, getConsoleSink } from "@logtape/logtape";await configure({ sinks: { console: getConsoleSink() }, loggers: [ { category: "my-app", lowestLevel: "info", sinks: ["console"] } ]});
Existing applications using winston or Pino can immediately benefit from LogTape-enabled libraries by installing the appropriate adapter. For comprehensive migration guidance and detailed feature documentation, visit our documentation site.
The 1.0.0 release represents not just a version number, but a commitment to the stability and maturity that production applications require. We're excited to see what you'll build with LogTape.
Takahe has limited support for this type: See Original Article