Skip to main content

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.

PropertyTypeDescription
ctx.auth.userIdstring | nullLumio user ID of the account owner
ctx.auth.accountIdstring | nullLumio account ID
ctx.auth.rolestringAlways "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.

PropertyTypeDescription
ctx.user.idstringPlatform user ID of the sender
ctx.user.namestringUsername (login name)
ctx.user.displayNamestringDisplay name (may include capitalization, unicode)
ctx.user.platformstring"twitch", "youtube", "kick", "trovo", or "discord"
ctx.user.rolestring"everyone", "follower", "subscriber", "vip", "moderator", or "broadcaster"
ctx.user.isModbooleanWhether the user is a moderator
ctx.user.isVipbooleanWhether the user is a VIP
ctx.user.isSubbooleanWhether the user is a subscriber

ctx.auth vs ctx.user

ctx.authctx.user
WhoThe streamer/account ownerThe viewer in chat
When availableAlwaysnull for timers and some events
Use caseAccount-level lookups, configUser-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.

PropertyTypeDescription
ctx.configRecord<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\}).

MethodSignatureDescription
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.

MethodSignatureDescription
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.

MethodSignatureDescription
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.

ParameterTypeDescription
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

Propertycommandkeywordpatterneventtimermoderate
ctx.authYesYesYesYesYesYes
ctx.userYesYesYesDependsnullYes
ctx.configYesYesYesYesYesYes
ctx.dbYesYesYesYesYesYes
ctx.cacheYesYesYesYesYesYes
ctx.secretsYesYesYesYesYesYes
ctx.fetchYesYesYesYesYesYes
ctx.deferYesYesYesYesYesYes

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.