Pop Context
Every command, snap, and event handler receives a pop object (or EventPop for event handlers) as its first argument. It bundles identity, reply helpers, caching, storage, i18n, and more into one place.
Identity
Section titled “Identity”pop.user // discord.js User — who triggered this interactionpop.member // GuildMember | null — the member in the current serverpop.guild // Guild | null — the current serverpop.channel // TextBasedChannel | null — the current channelpop.client // PopiiClient — the Popii client instancepop.locale // string — the user's Discord locale ("en-US", "fr", etc.)Replying
Section titled “Replying”// Simple text replyawait pop.reply("Hello!");
// With optionsawait pop.reply({ content: "Hello!", ephemeral: true });
// With embeds and componentsawait pop.reply({ embeds: [myEmbed], components: [actionRow] });
// Defer then follow up (for slow operations)await pop.defer();const data = await fetchSomethingSlow();await pop.reply(data); // sends the follow-upReading options
Section titled “Reading options”Options from the slash command are available via pop.options. These are the same getters as discord.js’s CommandInteractionOptionResolver:
pop.options.getString("name") // string | nullpop.options.getUser("user") // User | nullpop.options.getInteger("count") // number | nullpop.options.getNumber("amount") // number | nullpop.options.getBoolean("enabled") // boolean | nullpop.options.getChannel("channel") // Channel | nullpop.options.getRole("role") // Role | nullpop.options.getMember("user") // GuildMember | nullPass true as the second argument to assert a required option (throws if absent, returns non-nullable type):
const target = pop.options.getUser("user", true); // User — not nullIf you declared a schema, skip pop.options and use pop.input instead — it’s fully typed and validated:
pop.input.text // string — typed from your Zod schemapop.input.count // numberIn-process caching
Section titled “In-process caching”Cache any async result with a TTL. The second call within the window returns the cached value without hitting the source again:
const profile = await pop.cache(`profile:${pop.user.id}`, 60_000, async () => { return db.query("SELECT * FROM users WHERE id = ?").get(pop.user.id);});The cache is in-process and clears on restart. Use it for frequently-read, slow-to-fetch values within a single bot session.
The Cooler — persistent key-value store
Section titled “The Cooler — persistent key-value store”The Cooler is a persistent KV store backed by Redis (if configured) or an in-process Map. Unlike the cache helper, values survive bot restarts when Redis is connected.
// Write with a TTLawait pop.cooler.set("last_seen:" + pop.user.id, Date.now(), 86_400_000); // 24h
// Readconst lastSeen = await pop.cooler.get<number>("last_seen:" + pop.user.id);
// Deleteawait pop.cooler.delete("last_seen:" + pop.user.id);Localization
Section titled “Localization”// pop.t() is shorthand for pop.translate()const msg = pop.t("welcome_message", { username: pop.user.username });Locale files live in src/locales/<locale>.json. Popii picks the right file based on pop.locale (the user’s Discord language setting).
{ "welcome_message": "Welcome, {{username}}!"}Scheduling background tasks
Section titled “Scheduling background tasks”Trigger a named task (from src/tasks/) immediately or after a delay:
await pop.schedule("send-report"); // run nowawait pop.schedule("send-report", { userId: "123" }, 60_000); // run in 1 minuteHTTP helpers
Section titled “HTTP helpers”Fetch JSON, text, or binary data without setting up headers manually:
const data = await pop.fetchJSON<WeatherData>("https://api.weather.com/current");const readme = await pop.fetchText("https://raw.githubusercontent.com/org/repo/README.md");const image = await pop.fetchBuffer("https://example.com/thumbnail.png");Attachments
Section titled “Attachments”await pop.reply({ content: "Here's your report:", files: [pop.createAttachment("./exports/report.csv", "report.csv")],});Packing data into custom IDs
Section titled “Packing data into custom IDs”Discord component customId fields are capped at 100 characters. pop.pack() encodes a JavaScript object into a compact string you can embed in a customId, and pop.snapData decodes it on the other side:
// In a command — embed context in the buttonconst customId = pop.pack("confirm-ban", { targetId: member.id, reason: "spam" });
// In the snap — read it backexport default snap({ prefix: "confirm-ban", async do(pop) { const { targetId, reason } = pop.snapData as { targetId: string; reason: string }; // proceed with the ban },});per-request locals
Section titled “per-request locals”pop.locals is the typed scratch pad populated by your middleware. Augment PopiiLocals to get types everywhere in your project:
declare module "popii-framework" { interface PopiiLocals { dbUser: { id: string; balance: number; premium: boolean }; guildConfig: Record<string, string>; }}After this, pop.locals.dbUser is typed in every command, snap, and middleware.
Global bot state
Section titled “Global bot state”Declare a typed state object in popiiClient() for mutable values that belong to the bot’s lifetime — not a request:
const client = popiiClient({ token: process.env.DISCORD_TOKEN!, state: { totalCommandsRun: 0, startedAt: Date.now(), },});
// In any command:pop.state.totalCommandsRun++;Global event bus
Section titled “Global event bus”Emit and listen to custom events anywhere in your bot. Useful for cross-plugin communication:
// Emit from a command or event handlerawait pop.client.global.emit("level-up", { userId: pop.user.id, newLevel: 10 });
// Listen from a plugin's setup hookclient.global.on("level-up", ({ userId, newLevel }) => { announceInChannel(`<@${userId}> reached level ${newLevel}!`);});