Bot Module Context
The BotModuleContext object is passed as the first argument (ctx) to every bot module handler. It provides access to the database, cache, config, secrets, external HTTP, and background jobs.
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", "trovo", 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
Read and write data in the extension's isolated PostgreSQL schema (ext_\{install_id\}).
| Method | Signature | Description |
|---|---|---|
get | (table: string, id: string) => Promise<Record<string, unknown> | null> | Get a row by ID |
list | (table: string) => Promise<Record<string, unknown>[]> | List all rows in a table |
insert | (table: string, row: Record<string, unknown>) => Promise<string> | Insert a row, returns the new row ID |
patch | (table: string, id: string, row: Record<string, unknown>) => Promise<void> | Update specific fields on a row |
delete | (table: string, id: string) => Promise<void> | Delete a row by ID |
// Insert a new record
const id = await ctx.db.insert("user_points", {
user_id: ctx.user.id,
balance: 100,
});
// Read it back
const record = await ctx.db.get("user_points", id);
// Update a field
await ctx.db.patch("user_points", id, { balance: 200 });
// List all records
const all = await ctx.db.list("user_points");
// Delete
await ctx.db.delete("user_points", id);
Tables must be defined in server/schema.ts. The extension cannot access core Lumio tables or other extensions' schemas.
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) => Promise<number> | Atomically increment a numeric value |
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");
See Cache & Background Jobs for limits and usage patterns.
ctx.secrets — Extension secrets
Read encrypted secrets stored via lumio secrets set or the dashboard.
| Method | Signature | Description |
|---|---|---|
get | (key: string) => Promise<string | null> | Read a secret value |
const apiKey = await ctx.secrets.get("EXTERNAL_API_KEY");
Secret values are never logged or included in error reports.
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();
Restrictions:
- HTTPS only
- No private IP ranges or localhost
- Only declared
egress.allowHosts - Maximum 10 calls per handler invocation
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.db.insert("point_history", {
user_id: ctx.user.id,
checked_at: Date.now(),
});
});
return { reply: `${ctx.user.name}: ${balance} points` };
});
Limits: 5 deferred calls per handler, 10-second timeout each. Errors are logged but not surfaced to chat. See Cache & Background Jobs for details.
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).
Moderation handler
interface ModerationResponse {
block: boolean; // Whether to block/delete the message
action?: "delete" | "timeout" | "ban";
duration?: number; // Timeout duration in seconds
reason?: string; // Audit log reason
}
Return { block: false } to allow the message through.
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 |
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.