Bot Module Context
The BotModuleContext object is passed as the first argument (ctx) to every bot module handler. It provides the account and sender, the resolved config, a per-install cache, external HTTP and background jobs.
ctx.db, ctx.cache, ctx.secrets, ctx.fetch, ctx.defer() and ctx.realtime work in every bot-module handler. ctx.db needs a version that declares a schema; without one every call throws ctx.db: this extension has no data schema.
ctx.auth - Account context
The account (broadcaster/streamer) that installed the extension.
| Property | Type | Description |
|---|---|---|
ctx.auth.userId | string | null | Lumio user ID of the account owner |
ctx.auth.accountId | string | null | Lumio account ID |
ctx.auth.role | string | Always "owner" for bot module contexts |
For bot module handlers, userId and accountId are always non-null - the extension is installed on a specific account. The string | null type is a forward-compatibility measure.
ctx.user - Chat sender
The viewer who triggered the handler. This is null for timer handlers and event handlers without a specific chat sender.
| Property | Type | Description |
|---|---|---|
ctx.user.id | string | Platform user ID of the sender |
ctx.user.name | string | Username (login name) |
ctx.user.displayName | string | Display name (may include capitalization, unicode) |
ctx.user.platform | string | "twitch", "youtube", "kick", or "discord" |
ctx.user.role | string | "everyone", "follower", "subscriber", "vip", "moderator", or "broadcaster" |
ctx.user.isMod | boolean | Whether the user is a moderator |
ctx.user.isVip | boolean | Whether the user is a VIP |
ctx.user.isSub | boolean | Whether the user is a subscriber |
ctx.auth vs ctx.user
ctx.auth | ctx.user | |
|---|---|---|
| Who | The streamer/account owner | The viewer in chat |
| When available | Always | null for timers and some events |
| Use case | Account-level lookups, config | User-specific replies, permissions |
Example:
import { command } from "@zaflun/lumio-sdk/server";
export const whoami = command("whoami", async (ctx) => {
return {
reply: `You are ${ctx.user.name} (${ctx.user.role}) on ${ctx.auth.accountId}'s channel`,
};
});
ctx.config - Resolved configuration
User-configurable settings defined in config_schema, with user overrides merged over extension defaults.
Config values are validated server-side against your config_schema before they reach the handler. You can safely access ctx.config.myField knowing it matches the declared type.
| Property | Type | Description |
|---|---|---|
ctx.config | Record<string, unknown> | Key-value map of resolved config values |
export const hello = command("hello", async (ctx) => {
const greeting = (ctx.config.greeting as string) ?? "Hello";
const showPoints = (ctx.config.showPoints as boolean) ?? true;
return { reply: `${greeting}, ${ctx.user.name}!` };
});
ctx.db - Extension database
Persistent rows in the tables your version declares in server/schema.ts (see Schema). Every call is scoped to the install's account: a handler never sees or changes another account's rows.
| Method | Resolves to | Description |
|---|---|---|
get(table, id) | row or null | One row by _id |
list(table, filters?) | rows | Newest first, at most 100 rows |
insert(table, values) | _id string | Inserts a row; required columns must be set |
patch(table, id, values) | row or null | Updates the given columns and _updated_at |
delete(table, id) | true / false | Whether a row was deleted |
filters is a list of { field, op, value } with op one of eq, neq, gt, gte, lt, lte, for example [{ field: "score", op: "gte", value: 10 }] (typed as DbFilter from @zaflun/lumio-sdk/server). Rows carry the system columns _id, _account_id, _created_at and _updated_at.
export const points = command("points", async (ctx, args) => {
const user = ctx.user?.name ?? "anon";
const [row] = await ctx.db.list("points", [{ field: "user", op: "eq", value: user }]);
if (row) {
await ctx.db.patch("points", row._id as string, { score: (row.score as number) + 1 });
} else {
await ctx.db.insert("points", { user, score: 1 });
}
return { reply: `${user} has points`, actions: [] };
});
Limits:
- 25
ctx.dbcalls per run. - Per installation and minute: 100 reads (
get/list) and 200 writes (insert/patch/delete), shared with the extension's declarative server functions. - Each call is cancelled after 200 ms in
commandandmoderatehandlers and after 2 s elsewhere; when no database connection is free in time, it throwsctx.db: db busy. - When the rate-limit store is unreachable, reads still run and writes throw
ctx.db: extension storage writes are unavailable.
ctx.cache - Scoped Redis cache
Fast temporary storage backed by Redis. Keys are automatically scoped to the extension installation.
| Method | Signature | Description |
|---|---|---|
get | (key: string) => Promise<unknown | null> | Read a cached value |
set | (key: string, value: unknown, ttl?: number) => Promise<void> | Store a value (default TTL: 1 hour) |
delete | (key: string) => Promise<void> | Remove a cached value |
increment | (key: string, by?: number, ttl?: number) => Promise<number> | Atomically increment a numeric value (creates it at 0; ttl applies only when the key is created) |
await ctx.cache.set("spam_score:user123", 5, 300); // 5-minute TTL
const score = await ctx.cache.get("spam_score:user123");
await ctx.cache.increment("message_count", 1);
await ctx.cache.delete("spam_score:user123");
Keys are [A-Za-z0-9:_-], at most 128 characters. Each install has at most 256 keys of at most 4 KB each; every key expires (default 1 hour, max 24 hours). A call over a limit throws. See Cache & Background Jobs for all limits and usage patterns.
ctx.secrets - Extension secrets
Read a secret you stored with lumio env set or the developer dashboard (Extensions → your extension → Secrets):
const apiKey = await ctx.secrets.get("WEATHER_API_KEY");
if (apiKey === null) {
return { reply: "This module is not configured yet.", actions: [] };
}
- Secrets belong to the extension, not to the streamer who installed it: every install reads the same values. Streamers never see or set them.
get(key)resolves to the value, ornullwhen no secret with that key exists. Keys match^[A-Z0-9_]{1,64}$; another key throwsinvalid secret key.- A changed or deleted secret reaches running handlers within 60 seconds.
- If Lumio cannot read the secrets right now, the call throws
ctx.secrets is unavailable. Catch it if your handler has a sensible fallback. - At most 10
ctx.secrets.getcalls per handler run. - Never put a secret into a chat reply, a log line or
ctx.cache.
ctx.fetch - External HTTP
Make HTTPS requests to external APIs. Only hostnames declared in the egress.allowHosts manifest field are permitted.
const res = await ctx.fetch("https://api.example.com/data", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ query: "test" }),
});
const data = await res.json();
ctx.fetch returns a small response object, not a full Response: status, ok, headers (plain object), json() and text(). A string body is sent as-is; an object body is sent as JSON with Content-Type: application/json.
Restrictions:
- HTTPS only, port 443 unless the manifest lists more in
egress.allowPorts - Only hosts declared in
egress.allowHosts; without anegressblock every request is denied - Public addresses only: hosts that resolve to private, loopback or link-local addresses are refused
- Redirects are not followed
- 10 calls per handler run; 5-second timeout per request (never longer than the handler's remaining time)
- Response body: at most 1 MB for
commandandmoderatehandlers, 10 MB for the others
A denied or failed request throws (for example ctx.fetch: egress denied: …).
ctx.defer() - Background jobs
Queue a function that executes after the handler returns. The handler can respond immediately while expensive work runs in the background.
| Parameter | Type | Description |
|---|---|---|
fn | () => Promise<void> | Async function to run in the background |
export const points = command("points", async (ctx) => {
const balance = (await ctx.cache.get(`pts:${ctx.user.id}`)) as number ?? 0;
ctx.defer(async () => {
await ctx.fetch("https://api.example.com/checks", {
method: "POST",
body: { user: ctx.user.id, balance },
});
});
return { reply: `${ctx.user.name}: ${balance} points` };
});
Deferred jobs run after the handler has returned (the reply is sent first) and only when the handler did not throw. Limits: 5 jobs per handler run, 10 seconds for all jobs of one run together, 10 jobs per minute per install. Jobs are not persisted. Errors are logged, never sent to chat. See Cache & Background Jobs for details.
ctx.realtime - Push to the extension's surfaces
Send a message to every rendered surface of this install (its overlay and widget layers and the editor panel), where useRealtime receives it. A chat vote can update the overlay this way:
export const vote = command("vote", async (ctx, args) => {
await ctx.db.insert("votes", { choice: args[0], user: ctx.user?.id });
ctx.realtime.push("poll:votes", { choice: args[0] });
return { reply: `${ctx.user?.name} voted ${args[0]}` };
});
topic is 1-64 characters of A-Z a-z 0-9 _ . : - and starts with a letter or digit; payload is any JSON value of at most 16 KiB (omitted = null). Pushes are sent after the handler returns, only when it did not throw. Limits: 32 pushes per handler run (a push over the limit, a bad topic or a too large payload throws), 300 per install and minute shared with the extension's server functions (more are dropped). Nothing is stored or replayed.
Handler return types
Command, keyword, pattern, timer, event handlers
interface HandlerResponse {
reply?: string; // Text to send to chat
actions?: ActionRequest[]; // Additional platform actions
}
interface ActionRequest {
action: string;
params: Record<string, unknown>;
}
Return null for silent handling (no reply, no action).
actions run after the reply is sent, at most once each, through the same permission gate as widget actions: the manifest must declare each action name in permissions. An install may run at most 120 actions per minute; every outcome appears in the install's log. See Bot Modules → Actions.
Moderation handler
interface ModerationResponse {
block: boolean; // Not read by the runtime - `action` decides
action?: "delete" | "timeout" | "ban";
duration?: number; // Timeout duration in seconds
reason?: string; // Passed to the platform with a timeout/ban; never stored in the audit log
}
defineModerate handlers return the same shape without block, plus "allow" and "warn":
interface ModerateResult {
action: "allow" | "delete" | "timeout" | "warn" | "ban";
duration?: number; // Timeout duration in seconds (default 600)
reason?: string; // Passed to the platform; never stored in the audit log
message?: string; // Chat text for "warn"
}
Lumio acts on the action field only. allow (or no action) lets the message through; delete, timeout, ban and warn become the chat actions chat:delete, chat:timeout (duration seconds, default 600), chat:ban and chat:send (message, else reason). The manifest must declare that action in permissions, or it is refused. block is not read: { block: true } without an action does nothing. The handler has a 500ms budget; a timeout or error lets the message through. See Bot Modules → Moderation.
Context availability by handler type
| Property | command | keyword | pattern | event | timer | moderate |
|---|---|---|---|---|---|---|
ctx.auth | Yes | Yes | Yes | Yes | Yes | Yes |
ctx.user | Yes | Yes | Yes | Depends | null | Yes |
ctx.config | Yes | Yes | Yes | Yes | Yes | Yes |
ctx.db | Yes | Yes | Yes | Yes | Yes | Yes |
ctx.cache | Yes | Yes | Yes | Yes | Yes | Yes |
ctx.secrets | Yes | Yes | Yes | Yes | Yes | Yes |
ctx.fetch | Yes | Yes | Yes | Yes | Yes | Yes |
ctx.defer | Yes | Yes | Yes | Yes | Yes | Yes |
ctx.realtime | Yes | Yes | Yes | Yes | Yes | Yes |
For event handlers, ctx.user is populated when the event has a specific chat sender. For events like twitch:stream_online that have no sender, ctx.user is null.
Rate limits per install
Besides the per-run call limits above, each installation has per-minute limits: 60 command runs, 100 keyword/pattern runs, 50 event runs, 200 cache operations and 10 deferred jobs. A trigger over its limit is skipped silently; a cache operation or deferred job over its limit throws or is skipped. Moderation and timer handlers are not rate-limited. The actions all handlers return share one cap of 120 per minute per install; actions over it are refused.