-
-
Notifications
You must be signed in to change notification settings - Fork 6
Description
Currently, BotKit has a fundamental limitation: one instance can only host one bot (one ActivityPub actor). This issue was originally raised in #15, where a user asked how to run multiple bots on the same server. The honest answer was that it's not currently possible—users must deploy separate server processes for each bot, even when those bots could logically share infrastructure.
This constraint is deeply embedded throughout the architecture: the Bot interface has a single identifier, the Repository stores data without actor scoping, and event handlers are attached directly to the Bot instance. Addressing this requires a fundamental redesign.
Proposed design
Core concept: separate Instance from Bot
The key insight is that the current Bot conflates two distinct concepts:
- Instance: The server infrastructure (KV store, message queue, HTTP handling)
- Bot: An individual ActivityPub actor with its own identity and behavior
By separating these, we can support multiple bots per instance naturally.
New API
Multi-bot mode
The new createInstance() function creates a server instance that can host multiple bots. Each bot is created via instance.createBot(), which accepts either a fixed identifier with a profile object (for static bots) or a dispatcher function (for dynamic bots).
import { createInstance, text } from "@fedify/botkit";
const instance = createInstance<void>({ kv: new DenoKvStore(kv) });For static bots, you provide an identifier string and a profile object. This is suitable when you have a known, fixed set of bots:
const greetBot = instance.createBot("greet", {
username: "greetbot",
name: "Greeting Bot",
});
greetBot.onFollow = async (session, { follower, followRequest }) => {
await followRequest.accept();
await session.publish(text`Welcome, ${follower}!`);
};For dynamic bots, you provide a dispatcher function that receives an identifier and returns a profile (or null if the identifier doesn't match). This is ideal for scenarios like "one bot per region" where you might have thousands of potential bots that are created on-demand from a database:
const weatherBots = instance.createBot(async (ctx, identifier) => {
// Return null for identifiers this dispatcher doesn't handle
if (!identifier.startsWith("weather_")) return null;
// Look up the region from the database
const regionCode = identifier.slice("weather_".length);
const region = await db.getRegion(regionCode);
if (region == null) return null;
// Return the bot profile
return {
username: identifier,
name: `${region.name} Weather Bot`,
};
});The handler registration API is identical for both static and dynamic bots. For dynamic bots, the session.bot property provides access to the current bot's identity, so handlers can determine which specific bot is being invoked:
weatherBots.onMention = async (session, { message }) => {
// session.bot contains the current bot's information
const regionCode = session.bot.identifier.slice("weather_".length);
const weather = await fetchWeather(regionCode);
await session.publish(text`Current weather: ${weather}`);
};
export default instance;Single-bot mode (backward compatible)
The existing createBot() function continues to work for single-bot use cases. It now returns an object that implements both Bot and Instance interfaces, so it can be used directly as a server while also accepting event handlers:
import { createBot, text } from "@fedify/botkit";
const bot = createBot<void>({
identifier: "mybot",
username: "mybot",
name: "My Bot",
kv: new DenoKvStore(kv),
});
bot.onFollow = async (session, event) => { ... };
export default bot; // Works as beforeWhy this design?
The API is orthogonal: both static and dynamic bots use instance.createBot(). The only difference is whether the first argument is a string (static) or a function (dynamic). Handler registration is identical for both.
This design provides a BotKit-level abstraction rather than exposing Fedify's internals. Unlike directly exposing setActorDispatcher() and setKeyPairsDispatcher(), BotKit manages key pairs and actor dispatching automatically. Users define bots and handlers without needing to understand Fedify's lower-level APIs.
The session.bot property gives handlers rich access to the current bot's identity. Instead of just providing a string identifier, handlers can access session.bot.identifier, session.bot.name, session.bot.username, and other profile information.
Terminology
We deliberately use BotKit-specific terms rather than ActivityPub jargon:
| ActivityPub | BotKit | Rationale |
|---|---|---|
| Federation | Instance | “Instance” is clearer for “server that hosts bots” |
| Actor | Bot | BotKit is about bots, not generic actors |
Implementation scope
- New types:
Instance,BotInfo, updateSessionto includebotproperty - New functions:
createInstance(),Instance#createBot() - Repository changes: scope all data by bot identifier (
["_botkit", "bots", identifier, ...]) - Internal routing: match incoming requests/activities to the correct bot and its handlers
- Automatic key management: generate and store key pairs per bot identifier
- Backward compatibility:
createBot()returnsBot & Instancefor single-bot use cases