Skip to content

Support multiple bots per instance #16

@dahlia

Description

@dahlia

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 before

Why 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

  1. New types: Instance, BotInfo, update Session to include bot property
  2. New functions: createInstance(), Instance#createBot()
  3. Repository changes: scope all data by bot identifier (["_botkit", "bots", identifier, ...])
  4. Internal routing: match incoming requests/activities to the correct bot and its handlers
  5. Automatic key management: generate and store key pairs per bot identifier
  6. Backward compatibility: createBot() returns Bot & Instance for single-bot use cases

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions