Hello, I'm an open source software engineer in my late 30s living in #Seoul, #Korea, and an avid advocate of #FLOSS and the #fediverse.
I'm the creator of @fedify, an #ActivityPub server framework in #TypeScript, @hollo, an ActivityPub-enabled microblogging software for single users, and @botkit, a simple ActivityPub bot framework.
@silverpill@partizan how do you make subscriber only posts work, when you generally only have followers as a collection to address to? (Unless you use direct addressing to every subscriber via like bto/bcc?)
Mitra has pay to subscribe feature, there are subscribers-only posts (compatible with other fedi services) and an integrated payment processor. It uses Monero, so transactions are private and can't be blocked by 3rd parties.
But legal compliance might not be possible in some countries. For example, EU plans to ban Monero by 2027.
We've been working hard to make Fedify more modular and easier to integrate with your favorite tools and platforms. From the core framework to database drivers, from CLI tools to web framework integrations—we've got you covered.
Our packages now include:
Core framework and CLI tools
Web framework integrations: Express, Hono, H3, Elysia, NestJS, Next.js, SvelteKit
ALT text detailsA table showing 16 Fedify packages with three columns: Package name, registry availability (JSR and npm links), and Description. The packages include the core @fedify/fedify framework, CLI toolchain, database drivers (PostgreSQL, Redis, SQLite, AMQP/RabbitMQ), web framework integrations (Express, Hono, H3, Elysia, NestJS, Next.js, SvelteKit, Cloudflare Workers), Deno KV integration, and testing utilities. Most packages are available on both JSR and npm registries, with some exceptions like @fedify/denokv (JSR only) and @fedify/elysia, @fedify/nestjs, @fedify/next (npm only).
We've been working hard to make Fedify more modular and easier to integrate with your favorite tools and platforms. From the core framework to database drivers, from CLI tools to web framework integrations—we've got you covered.
Our packages now include:
Core framework and CLI tools
Web framework integrations: Express, Hono, H3, Elysia, NestJS, Next.js, SvelteKit
ALT text detailsA table showing 16 Fedify packages with three columns: Package name, registry availability (JSR and npm links), and Description. The packages include the core @fedify/fedify framework, CLI toolchain, database drivers (PostgreSQL, Redis, SQLite, AMQP/RabbitMQ), web framework integrations (Express, Hono, H3, Elysia, NestJS, Next.js, SvelteKit, Cloudflare Workers), Deno KV integration, and testing utilities. Most packages are available on both JSR and npm registries, with some exceptions like @fedify/denokv (JSR only) and @fedify/elysia, @fedify/nestjs, @fedify/next (npm only).
ALT text detailsHackers' public
1st MEETUP @SEOUL
2025. 09. 14. 15:00 KST
서울특별시 성동구 상원길 26 튜링의 사과
Code As A Canvas : 코드에서 예술작품이 되기까지 (@jakeseo@hackers.pub)
폰트는 어떻게 만들어지는가 - NeoDGM 사례로 살펴보는 개발 후일담 (@dalgona@hackers.pub)
#Optique 0.3.0 is out with dependent options and flexible parser composition, shaped by feedback from @z9mb1's work migrating @fedify CLI from Cliffy to Optique.
We're releasing Optique 0.3.0 with several improvements that make building complex CLI applications more straightforward. This release focuses on expanding parser flexibility and improving the help system based on feedback from the community, particularly from the Fedify project's migration from Cliffy to Optique. Special thanks to @z9mb1 for her invaluable insights during this process.
What's new
Required Boolean flags with the new flag() parser for dependent options patterns
Flexible type defaults in withDefault() supporting union types for conditional CLI structures
Extended or() capacity now supporting up to 10 parsers (previously 5)
Enhanced merge() combinator that works with any object-producing parser, not just object()
Context-aware help using the new longestMatch() combinator
Version display support in both @optique/core and @optique/run
Structured output functions for consistent terminal formatting
Required Boolean flags with flag()
The new flag() parser creates Boolean flags that must be explicitly provided. While option() defaults to false when absent, flag() fails parsing entirely if not present. This subtle difference enables cleaner patterns for dependent options.
Consider a scenario where certain options only make sense when a mode is explicitly enabled:
import { flag, object, option, withDefault } from "@optique/core/parser";import { integer } from "@optique/core/valueparser";// Without the --advanced flag, these options aren't availableconst parser = withDefault( object({ advanced: flag("--advanced"), maxThreads: option("--threads", integer()), cacheSize: option("--cache-size", integer()) }), { advanced: false as const });// Usage:// myapp → { advanced: false }// myapp --advanced → Error: --threads and --cache-size required// myapp --advanced --threads 4 --cache-size 100 → Success
This pattern is particularly useful for confirmation flags (--yes-i-am-sure) or mode switches that fundamentally change how your CLI behaves.
Union types in withDefault()
Previously, withDefault() required the default value to match the parser's type exactly. Now it supports different types, creating union types that enable conditional CLI structures:
const conditionalParser = withDefault( object({ server: flag("-s", "--server"), port: option("-p", "--port", integer()), host: option("-h", "--host", string()) }), { server: false as const });// Result type is now a union:// | { server: false }// | { server: true, port: number, host: string }
This change makes it much easier to build CLIs where different flags enable different sets of options, without resorting to complex or() chains.
More flexible merge() combinator
The merge() combinator now accepts any parser that produces object-like values. Previously limited to object() parsers, it now works with withDefault(), map(), and other transformative parsers:
const transformedConfig = map( object({ host: option("--host", string()), port: option("--port", integer()) }), ({ host, port }) => ({ endpoint: `${host}:${port}` }));const conditionalFeatures = withDefault( object({ experimental: flag("--experimental"), debugLevel: option("--debug-level", integer()) }), { experimental: false as const });// Can now merge different parser typesconst appConfig = merge( transformedConfig, // map() result conditionalFeatures, // withDefault() parser object({ // traditional object() verbose: option("-v", "--verbose") }));
This improvement came from recognizing that many parsers ultimately produce objects, and artificially restricting merge() to only object() parsers was limiting composition patterns.
Context-aware help with longestMatch()
The new longestMatch() combinator selects the parser that consumes the most input tokens. This enables sophisticated help systems where command --help shows help for that specific command rather than global help:
const normalParser = object({ help: constant(false), command: or( command("list", listOptions), command("add", addOptions) )});const contextualHelp = object({ help: constant(true), commands: multiple(argument(string())), helpFlag: flag("--help")});const cli = longestMatch(normalParser, contextualHelp);// myapp --help → Shows global help// myapp list --help → Shows help for 'list' command// myapp add --help → Shows help for 'add' command
The run() functions in both @optique/core/facade and @optique/run now use this pattern automatically, so your CLI gets context-aware help without any additional configuration.
Version display support
Both @optique/core/facade and @optique/run now support version display through --version flags and version commands. See the runners documentation for details:
// @optique/run - simple APIrun(parser, { version: "1.0.0", // Adds --version flag help: "both"});// @optique/core/facade - detailed controlrun(parser, "myapp", args, { version: { mode: "both", // --version flag AND version command value: "1.0.0", onShow: process.exit }});
The API follows the same pattern as help configuration, keeping things consistent and predictable.
Structured output functions
The new output functions in @optique/run provide consistent terminal formatting with automatic capability detection. Learn more in the messages documentation:
import { print, printError, createPrinter } from "@optique/run";import { message } from "@optique/core/message";// Standard output with automatic formattingprint(message`Processing ${filename}...`);// Error output to stderr with optional exitprintError(message`File ${filename} not found`, { exitCode: 1 });// Custom printer for specific needsconst debugPrint = createPrinter({ stream: "stderr", colors: true, maxWidth: 80});debugPrint(message`Debug: ${details}`);
These functions automatically detect terminal capabilities and apply appropriate formatting, making your CLI output consistent across different environments.
Breaking changes
While we've tried to maintain backward compatibility, there are a few changes to be aware of:
The help option in @optique/run no longer accepts "none". Simply omit the option to disable help.
Custom parsers implementing getDocFragments() need to update their signature to use DocState<TState> instead of direct state values.
The object() parser now uses greedy parsing, attempting to consume all matching fields in one pass. This shouldn't affect most use cases but may change parsing order in complex scenarios.
Upgrading to 0.3.0
To upgrade to Optique 0.3.0, update both packages:
These improvements came from real-world usage and community feedback. We're particularly interested in hearing how the new dependent options patterns work for your use cases, and whether the context-aware help system meets your needs.
As always, you can find complete documentation at optique.dev and file issues or suggestions on GitHub.
#Optique 0.3.0 is out with dependent options and flexible parser composition, shaped by feedback from @z9mb1's work migrating @fedify CLI from Cliffy to Optique.
We're releasing Optique 0.3.0 with several improvements that make building complex CLI applications more straightforward. This release focuses on expanding parser flexibility and improving the help system based on feedback from the community, particularly from the Fedify project's migration from Cliffy to Optique. Special thanks to @z9mb1 for her invaluable insights during this process.
What's new
Required Boolean flags with the new flag() parser for dependent options patterns
Flexible type defaults in withDefault() supporting union types for conditional CLI structures
Extended or() capacity now supporting up to 10 parsers (previously 5)
Enhanced merge() combinator that works with any object-producing parser, not just object()
Context-aware help using the new longestMatch() combinator
Version display support in both @optique/core and @optique/run
Structured output functions for consistent terminal formatting
Required Boolean flags with flag()
The new flag() parser creates Boolean flags that must be explicitly provided. While option() defaults to false when absent, flag() fails parsing entirely if not present. This subtle difference enables cleaner patterns for dependent options.
Consider a scenario where certain options only make sense when a mode is explicitly enabled:
import { flag, object, option, withDefault } from "@optique/core/parser";import { integer } from "@optique/core/valueparser";// Without the --advanced flag, these options aren't availableconst parser = withDefault( object({ advanced: flag("--advanced"), maxThreads: option("--threads", integer()), cacheSize: option("--cache-size", integer()) }), { advanced: false as const });// Usage:// myapp → { advanced: false }// myapp --advanced → Error: --threads and --cache-size required// myapp --advanced --threads 4 --cache-size 100 → Success
This pattern is particularly useful for confirmation flags (--yes-i-am-sure) or mode switches that fundamentally change how your CLI behaves.
Union types in withDefault()
Previously, withDefault() required the default value to match the parser's type exactly. Now it supports different types, creating union types that enable conditional CLI structures:
const conditionalParser = withDefault( object({ server: flag("-s", "--server"), port: option("-p", "--port", integer()), host: option("-h", "--host", string()) }), { server: false as const });// Result type is now a union:// | { server: false }// | { server: true, port: number, host: string }
This change makes it much easier to build CLIs where different flags enable different sets of options, without resorting to complex or() chains.
More flexible merge() combinator
The merge() combinator now accepts any parser that produces object-like values. Previously limited to object() parsers, it now works with withDefault(), map(), and other transformative parsers:
const transformedConfig = map( object({ host: option("--host", string()), port: option("--port", integer()) }), ({ host, port }) => ({ endpoint: `${host}:${port}` }));const conditionalFeatures = withDefault( object({ experimental: flag("--experimental"), debugLevel: option("--debug-level", integer()) }), { experimental: false as const });// Can now merge different parser typesconst appConfig = merge( transformedConfig, // map() result conditionalFeatures, // withDefault() parser object({ // traditional object() verbose: option("-v", "--verbose") }));
This improvement came from recognizing that many parsers ultimately produce objects, and artificially restricting merge() to only object() parsers was limiting composition patterns.
Context-aware help with longestMatch()
The new longestMatch() combinator selects the parser that consumes the most input tokens. This enables sophisticated help systems where command --help shows help for that specific command rather than global help:
const normalParser = object({ help: constant(false), command: or( command("list", listOptions), command("add", addOptions) )});const contextualHelp = object({ help: constant(true), commands: multiple(argument(string())), helpFlag: flag("--help")});const cli = longestMatch(normalParser, contextualHelp);// myapp --help → Shows global help// myapp list --help → Shows help for 'list' command// myapp add --help → Shows help for 'add' command
The run() functions in both @optique/core/facade and @optique/run now use this pattern automatically, so your CLI gets context-aware help without any additional configuration.
Version display support
Both @optique/core/facade and @optique/run now support version display through --version flags and version commands. See the runners documentation for details:
// @optique/run - simple APIrun(parser, { version: "1.0.0", // Adds --version flag help: "both"});// @optique/core/facade - detailed controlrun(parser, "myapp", args, { version: { mode: "both", // --version flag AND version command value: "1.0.0", onShow: process.exit }});
The API follows the same pattern as help configuration, keeping things consistent and predictable.
Structured output functions
The new output functions in @optique/run provide consistent terminal formatting with automatic capability detection. Learn more in the messages documentation:
import { print, printError, createPrinter } from "@optique/run";import { message } from "@optique/core/message";// Standard output with automatic formattingprint(message`Processing ${filename}...`);// Error output to stderr with optional exitprintError(message`File ${filename} not found`, { exitCode: 1 });// Custom printer for specific needsconst debugPrint = createPrinter({ stream: "stderr", colors: true, maxWidth: 80});debugPrint(message`Debug: ${details}`);
These functions automatically detect terminal capabilities and apply appropriate formatting, making your CLI output consistent across different environments.
Breaking changes
While we've tried to maintain backward compatibility, there are a few changes to be aware of:
The help option in @optique/run no longer accepts "none". Simply omit the option to disable help.
Custom parsers implementing getDocFragments() need to update their signature to use DocState<TState> instead of direct state values.
The object() parser now uses greedy parsing, attempting to consume all matching fields in one pass. This shouldn't affect most use cases but may change parsing order in complex scenarios.
Upgrading to 0.3.0
To upgrade to Optique 0.3.0, update both packages:
These improvements came from real-world usage and community feedback. We're particularly interested in hearing how the new dependent options patterns work for your use cases, and whether the context-aware help system meets your needs.
As always, you can find complete documentation at optique.dev and file issues or suggestions on GitHub.
Recently, whenever I write or share (boost) a post on my hollo.social instance, the server consistently crashes immediately afterward, and I haven't found the cause yet. Since I haven't received similar reports from other Hollo users, I suspect it might be an issue with my deployment configuration…
I’m Jaeyeol, a software engineer who loves Neovim and Zed.
I see myself as a pragmatic builder — I focus on creating software that I’d genuinely want to use, made for people rather than for technology’s sake.
Beyond coding, I also experiment actively within developer communities, often initiating gatherings and exploring new ways for people to connect and share.
One of my current personal projects happens to be in the fediverse space:
cosmoslide — a slideshare-like service for sharing presentations across the fediverse.
It’s built with NestJS (@fedify/nestjs) for the backend, Next.js for the frontend (most of it is vibe-coded, so I’ll probably rewrite it later 😅)
The project is still in development, with an expected first release between late September and mid October.
It started as a personal experiment, but I’m excited to see how it may contribute to the broader fediverse ecosystem.
I enjoy meeting others who are curious about new ideas, whether in technology or in community.
I'm currently improving Optique's façade API, and in the process, I've added a few more parsers. Optique 0.3.0 might have some breaking changes on the API side. Of course, these changes will be thoroughly documented in the changelog.
The development of fedify and botkit are handled splendidly, and all the progress is delightful to behold. Thanks to everyone involved for all the great work you do!
We're excited to announce the release of BotKit 0.3.0! This release marks a significant milestone as #BotKit now supports #Node.js alongside #Deno, making it accessible to a wider audience. The minimum required Node.js version is 22.0.0. This dual-runtime support means you can now choose your preferred #JavaScript runtime while building #ActivityPub#bots with the same powerful BotKit APIs.
One of the most requested features has landed: poll support! You can now create interactive polls in your #bot messages, allowing followers to vote on questions with single or multiple-choice options. Polls are represented as ActivityPub Question objects with proper expiration times, and your bot can react to votes through the new onVote event handler. This feature enhances engagement possibilities and brings BotKit to feature parity with major #fediverse platforms like Mastodon and Misskey.
The web frontend has been enhanced with a new followers page, thanks to the contribution from Hyeonseo Kim (@gaebalgom)! The /followers route now displays a paginated list of your bot's followers, and the follower count on the main profile page is now clickable, providing better visibility into your bot's audience. This improvement makes the web interface more complete and user-friendly.
For developers looking for alternative storage backends, we've introduced the SqliteRepository through the new @fedify/botkit-sqlite package. This provides a production-ready SQLite-based storage solution with ACID compliance, write-ahead logging (WAL) for optimal performance, and proper indexing. Additionally, the new @fedify/botkit/repository module offers MemoryCachedRepository for adding an in-memory cache layer on top of any repository implementation, improving read performance for frequently accessed data.
This release also includes an important security update: we've upgraded to #Fedify 1.8.8, ensuring your bots stay secure and compatible with the latest ActivityPub standards. The repository pattern has been expanded with new interfaces and types like RepositoryGetMessagesOptions, RepositoryGetFollowersOptions, and proper support for polls storage through the KvStoreRepositoryPrefixes.polls option, providing more flexibility for custom implementations.
@hongminhee I love what you’ve done with this library! I’m already pushing my coworkers to try to get them to adopt it. I’ve been frustrated by yargs for years.
>moving on AT Proto is much simpler for users than moving on ActivityPub, where you don't get to keep your posts currently
Can keep the posts too if your server implements FEP-ef61.
>So if the government decides to crack down on your fediverse instance, you're more screwed than if they crack down on Bluesky PBC or bluesky.social
In the worst case you just sign up on another Fediverse instance. The rest of the network is still there and all connections can be restored.
If Bluesky shuts down, it's over. They control almost all infrastructure. They control did:plc which is centralized. They have developers, moderators, and they have funding. Once all of that disappears, only a tiny group of power users remains who will burn out in a month.
New: SocialHub and the Substrate of Decentralised Networks
SocialHub, one of the primary forums to talk about the #fediverse and #ActivityPub, has been struggling how to continue the operation. Decentralised networks need a coordination layer, but how to build this in a decentralised manner?
The issue of SocialHub is an interesting one, because where we are today is an odd situation where you have activitypub developers fragmented across multiple collaborative channels.
Some discuss their issues on their respective repositories only
Some discuss on SocialHub
Some discuss on Matrix channels
However, the bottom line truth is as follows: every ActivityPub developer is on the fediverse, ergo why shouldn't ActivityPub-focused discussions take place on the fediverse as well?
Up until this year, SocialHub has been an island separate from the fediverse. I used this analogy in my talk at fedicon to describe how lonely starting a community can be.
To SocialHub's credit, they have created a community of ActivityPub developers that exists to this day, kudos to them! The question remains now whether SocialHub performing their function adequately — to bring together ActivityPub developers of all stripes.
That's a question worth exploring in and of itself.