Client Configuration
popiiClient() is the single entry point for creating a Popii bot. Pass a config object; only token is required. Everything else has a sensible default.
import { popiiClient } from "popii-framework";
const client = popiiClient({ token: process.env.DISCORD_TOKEN!,});
client.start();Connection
Section titled “Connection”token — string · required
Section titled “token — string · required”Your Discord bot token. Keep it in .env — never commit it.
token: process.env.DISCORD_TOKEN!mode — "websocket" | "http" · default "websocket"
Section titled “mode — "websocket" | "http" · default "websocket"”"websocket"— Standard Gateway connection. Works everywhere, recommended for most bots."http"— HTTP interactions endpoint for serverless or edge deployments. RequirespublicKeyandport.
mode: "http",publicKey: process.env.DISCORD_PUBLIC_KEY!,port: 3000,devGuildId — string
Section titled “devGuildId — string”A guild ID to sync slash commands to during development. Guild commands sync instantly; without this, global sync can take up to an hour.
devGuildId: process.env.DEV_GUILD_ID,Remove or leave undefined in production.
Text commands
Section titled “Text commands”prefix — string | (message) => string | string[] | null
Section titled “prefix — string | (message) => string | string[] | null”Enables legacy prefix-based text commands. Without this, only slash commands work.
prefix: "!", // all commands accept !commandNameprefix: msg => msg.guild?.id === "123" ? "?" : "!", // dynamic prefix per guildOwners
Section titled “Owners”owners — string[]
Section titled “owners — string[]”Discord user IDs with bot-owner privileges. Commands marked ownerOnly: true are restricted to these users.
owners: ["176341399966244864"],Storage
Section titled “Storage”redisUrl — string
Section titled “redisUrl — string”Enables Redis-backed storage: persistent cooldowns, the Cooler KV store, pub/sub for cross-shard messaging.
redisUrl: "redis://localhost:6379",redisUrl: process.env.REDIS_URL,redis — string | object
Section titled “redis — string | object”Alternative to redisUrl. Accepts a connection string or an ioredis options object.
storage — PopiiStorageAdapter
Section titled “storage — PopiiStorageAdapter”Provide a fully custom storage adapter — any backend that implements the get/set/zadd interface. The built-in default is an in-process Map (non-persistent).
Intents & partials
Section titled “Intents & partials”Popii infers both from your event filenames automatically. Override only when you need intents that have no corresponding event file.
intents — number[]
Section titled “intents — number[]”import { GatewayIntentBits } from "discord.js";
intents: [GatewayIntentBits.Guilds, GatewayIntentBits.GuildMessages],partials — Partials[]
Section titled “partials — Partials[]”import { Partials } from "discord.js";
partials: [Partials.Message, Partials.Reaction],Commands
Section titled “Commands”Fine-grained control over command loading and syncing.
commands: { dir: "./src/commands", // default folderRouting: true, // src/commands/config/set.ts → /config set autoSync: true, // sync to Discord on every startup},| Option | Type | Default | Description |
|---|---|---|---|
dir | string | "./src/commands" | Directory Popii scans for command files |
folderRouting | boolean | true | Map nested folders to subcommand groups |
autoSync | boolean | true | Sync slash commands to Discord at startup. Set to false in production and use popii sync instead |
help — boolean | object
Section titled “help — boolean | object”Controls the built-in /help command that lists all registered commands.
help: false, // disable entirelyhelp: { enabled: true, command: "commands", // /commands instead of /help},pluginCommands
Section titled “pluginCommands”Suppress or patch commands that plugins bundle, without touching the plugin source.
pluginCommands: { disable: ["daily"], // don't register this command at all overrides: [ { name: "balance", ownerOnly: true }, // restrict to owners { name: "leaderboard", cooldown: 10_000 }, // add a cooldown ],},User commands in src/commands/ always take priority over plugin commands regardless of this config.
Plugins
Section titled “Plugins”plugins — PopiiPlugin[]
Section titled “plugins — PopiiPlugin[]”The plugin array. Plugins initialize in list order; if plugin B depends on A, put A first.
import { sqlitePlugin, errorHandlerPlugin, webPlugin } from "popii-framework";
plugins: [ sqlitePlugin(), errorHandlerPlugin(), webPlugin({ port: 3000 }),],autoDiscover — boolean · default false
Section titled “autoDiscover — boolean · default false”When true, Popii scans node_modules for any installed popii-plugin-* package (identified by a "popii" field in package.json) and loads it automatically.
pluginConfig — Record<string, unknown>
Section titled “pluginConfig — Record<string, unknown>”Pass config to auto-discovered plugins by their package name:
autoDiscover: true,pluginConfig: { "popii-plugin-economy": { currencySymbol: "🪙" },},Bot status
Section titled “Bot status”status — string | string[]
Section titled “status — string | string[]”Activity text shown in the member list. An array rotates on a timer.
status: "Serving {{guilds}} servers", // single static statusstatus: ["Serving {{guilds}} servers", "Use /help"], // rotatesTemplate tokens: {{guilds}}, {{users}}, {{shardId}}
statusInterval — number · default 30000
Section titled “statusInterval — number · default 30000”Milliseconds between status rotations when status is an array.
presenceStrategy — "local" | "coordinated" · default "local"
Section titled “presenceStrategy — "local" | "coordinated" · default "local"”Controls how rotation is coordinated across shards.
"local"— each shard picks its own status independently."coordinated"— shard 0 broadcasts the active status to all other shards via Redis pub/sub so they all show the same string. Requires Redis.
Directory overrides
Section titled “Directory overrides”Every auto-loaded directory can be changed:
events: { dir: "./src/events" },snaps: { dir: "./src/snaps" },middlewares: { dir: "./src/middlewares" },tasks: { dir: "./src/tasks" },Localization
Section titled “Localization”locales: { dir: "./src/locales", // directory with <locale>.json files default: "en-US", // fallback when a key is missing in the user's locale url: "https://cdn.example.com/locales", // fetch from a remote URL instead},See the Localization guide for full details.
Error handling
Section titled “Error handling”onError — (error, pop) => void
Section titled “onError — (error, pop) => void”Called when a command or snap throws an error that is not caught by any middleware.
onError(error, pop) { console.error(`Command failed for ${pop.user.tag}:`, error);},onGlobalError — (error) => void
Section titled “onGlobalError — (error) => void”Called for unhandled promise rejections and uncaught exceptions not tied to a command.
onGlobalError(error) { Sentry.captureException(error);},Logging
Section titled “Logging”logger — PopLogger
Section titled “logger — PopLogger”Replace the built-in logger with your own. Must implement info, warn, error, and debug.
import pino from "pino";
const pinoLogger = pino();
logger: { info: (msg, ...args) => pinoLogger.info(args, msg), warn: (msg, ...args) => pinoLogger.warn(args, msg), error: (msg, ...args) => pinoLogger.error(args, msg), debug: (msg, ...args) => pinoLogger.debug(args, msg),},fileLogging — boolean · default false
Section titled “fileLogging — boolean · default false”Write rotating log files to ./logs/. Useful for debugging on a VPS where you can’t tail the process output live.
structuredLogs — boolean · default false
Section titled “structuredLogs — boolean · default false”Emit newline-delimited JSON instead of human-readable text. Pair with log aggregation tools like Datadog or Loki.
state — TState
Section titled “state — TState”Initial value for pop.state and client.state. Shared across every handler, persisted to Redis/storage between restarts.
state: { totalCommandsRun: 0, startedAt: Date.now(),},TypeScript infers the state type from this value — pop.state.totalCommandsRun is typed as number everywhere.
stateTtlMs — number · default 7_776_000_000 (90 days)
Section titled “stateTtlMs — number · default 7_776_000_000 (90 days)”How long persisted state lives in Redis before expiring.
Client instance
Section titled “Client instance”popiiClient() returns a PopiiClient with these methods:
| Method | Description |
|---|---|
client.start() | Connect to Discord and begin listening |
client.stop() | Graceful shutdown — drains active executions before disconnecting |
client.reload() | Hot-reload commands, events, snaps, and middleware without restarting |
client.schedule(name, payload?, delayMs?) | Trigger a named task from outside a handler |
client.broadcast(event, data) | Emit a cross-shard event (requires Redis) |
client.isPremium(userId, guildId?) | Check premium status via payPlugin |
client.extend(name, factory) | Add a custom property to every pop context |
client.loadPlugin(plugin) | Load a plugin at runtime |
client.unloadPlugin(name) | Unload a plugin at runtime |
client.reloadPlugin(name) | Reload a single plugin at runtime |
client.setPresence(status) | Update the bot’s activity string |
client.global.emit(event, data?) | Emit a custom in-process event |
client.global.on(event, listener) | Listen to a custom in-process event |