Server Functions
Server functions are optional server-side logic that runs on the Lumio API or in an isolated worker. They let extensions persist structured data, query databases, and use secrets. Handler-based server functions cannot call external APIs; use a bot module for outbound requests.
Enabling server functions
Add "server": true to lumio.config.json:
{
"server": true
}
This enables the server/ directory and activates the server runtime for your extension.
Two styles
| Style | Use case | Overhead |
|---|---|---|
| Declarative | Simple CRUD on tables (queryRows, insertRow, patchRow, deleteRow) | Minimal - transpiles to parameterized SQL, no JS runtime |
| Handler-based | Complex logic, conditional writes (query(), mutation(), action()) | Low - runs in a V8 isolate on the Bot Module Worker with memory and time limits |
Execution flow
Extension Client (browser)
-> useMutation("addRule") / useQuery("getRules") sends only { fn, params }
-> postMessage to Lumio host (dashboard, OBS overlay or widget)
-> host forwards with its own credential (session, or the overlay/widget token)
-> Lumio API: POST /v1/extension-installs/{install_id}/{query|mutate}/{functionName}
-> caller + surface from the credential, rate limit check
-> definition looked up by name in the install's published functions.json
-> editorOnly check (definition), arg() values bound as typed parameters
-> Declarative: typed, parameterized SQL -> PostgreSQL (ext_{extension_id}.*)
-> Handler: the API re-runs the call as /execute/{functionName}; the
Bot Module Worker runs the published server.js in a V8 isolate
-> ctx.db / ctx.cache / ctx.secrets
-> Response serialized to JSON
-> postMessage back to extension
Your extension never sends a table name, a filter or an editorOnly flag: the API takes all of that from the functions.json that lumio build wrote and lumio deploy uploaded with the version. Calling a name the installed version does not export rejects with Unknown server function: <name>.
Rate limits
| Function type | Limit | Applies to |
|---|---|---|
queryRows | 100 calls/minute | Per installation, shared with the bot module's ctx.db.get/list |
insertRow / patchRow / deleteRow | 200 calls/minute | Per installation, shared with the bot module's ctx.db.insert/patch/delete |
query() | 100 calls/minute | Per installation, shared with queryRows |
mutation() | 50 calls/minute | Per installation |
action() | 20 calls/minute | Per installation |
Database isolation
Each extension gets its own isolated PostgreSQL schema: ext_\{extension_id\}. Tables defined in server/schema.ts live in this schema:
ext_88b2478a_39c7_468e_bebd_14e52fd97ac0.rules
ext_88b2478a_39c7_468e_bebd_14e52fd97ac0.votes
No extension can read or write another extension's tables, and every row is scoped to the account of the install (see Storage scoping). The Lumio API enforces this twice: every statement it generates carries the install's account, and the database itself applies row-level security on every extension table, so a row of another account is neither visible nor writable. Server functions run as a database role that can only read and write rows; it cannot create, drop or truncate tables.
Declarative queries return at most 1000 rows (100 when you set no limit), in created_at order unless the definition sets orderBy, and are cancelled after 2 seconds. When the rate-limit store is unreachable, reads still run and writes are refused (503).
File structure
server/
├── schema.ts <- Table definitions (defineSchema + defineTable)
└── functions.ts <- Function exports (queryRows, action, etc.)
Both files are optional. An extension can have server: true but only use schema for useExtensionStorage() type safety, without defining custom functions.
When to use server functions
| Scenario | Recommendation |
|---|---|
| Simple toggle / text setting | useExtensionStorage() - no server needed |
| List of items (rules, songs, votes) | Declarative server functions + defineTable |
| Aggregations (count, sum, averages) | Handler-based query() |
| External API calls (sports scores, weather) | Bot-module handler with declared egress hosts |
| User-specific data (per-viewer state) | Handler-based with ctx.auth.userId |
| Secrets (API keys) | Handler-based action() with ctx.secrets |