Handler-based Functions
Handler-based server functions use query(), mutation(), and action(). They run JavaScript inside a V8 isolate with access to the database, secrets, and (for actions) external HTTP APIs.
When to use handler-based
| Use case | Function type |
|---|---|
| Complex query with aggregation | query() |
| Conditional logic before write | mutation() |
| External HTTP API calls | action() |
| Reading secrets | action() |
query()
A read-only function. Has access to ctx.db but not ctx.fetch or ctx.secrets.
// server/functions.ts
import { query, v } from "@zaflun/lumio-sdk/server";
export const getLeaderboard = query({
args: { limit: v.number() },
handler: async (ctx, args) => {
const rows = await ctx.db.query("votes", {
orderBy: "score",
orderDir: "desc",
limit: args.limit,
});
return rows;
},
});
mutation()
A read-write function. Has access to ctx.db and ctx.auth. Can read and write rows conditionally.
import { mutation, v } from "@zaflun/lumio-sdk/server";
export const castVote = mutation({
args: { ruleId: v.string(), choice: v.string() },
handler: async (ctx, args) => {
const userId = ctx.auth.userId;
if (!userId) throw new Error("Must be authenticated to vote");
// Check for existing vote
const existing = await ctx.db.get("votes", {
filter: { userId, ruleId: args.ruleId },
});
if (existing) {
// Update existing vote
await ctx.db.patch("votes", existing.id, { choice: args.choice });
} else {
// Insert new vote
await ctx.db.insert("votes", {
userId,
ruleId: args.ruleId,
choice: args.choice,
});
}
return { success: true };
},
});
action()
A full-capability function. Has access to ctx.db, ctx.auth, ctx.fetch, and ctx.secrets. Suitable for external API calls.
import { action, v } from "@zaflun/lumio-sdk/server";
export const getScoreboard = action({
args: { sport: v.string(), league: v.string() },
handler: async (ctx, args) => {
const apiKey = ctx.secrets.get("ESPN_API_KEY");
const res = await ctx.fetch(
`https://site.api.espn.com/apis/site/v2/sports/${args.sport}/${args.league}/scoreboard`,
{
headers: { "Authorization": `Bearer ${apiKey}` },
}
);
if (!res.ok) {
throw new Error(`ESPN API error: ${res.status}`);
}
const data = await res.json();
return data.events.slice(0, 5);
},
});
The ctx object
All handler-based functions receive a ctx (context) object as the first argument:
ctx.db
Database access for the extension's isolated schema:
// Get a single row by ID
const row = await ctx.db.get("rules", "some-uuid");
// Query rows with filter
const rows = await ctx.db.query("rules", {
filter: { revealed: true },
orderBy: "created_at",
limit: 10,
});
// Insert a row
const newRow = await ctx.db.insert("rules", {
text: "Complete the game without dying",
revealed: false,
});
// Update fields
await ctx.db.patch("rules", row.id, { revealed: true });
// Delete a row
await ctx.db.delete("rules", row.id);
ctx.auth
Identity information for the caller:
| Property | Type | Description |
|---|---|---|
userId | string | null | Lumio user ID, or null for unauthenticated |
accountId | string | Account that owns this installation |
role | "owner" | "editor" | "viewer" | "anonymous" | Caller's role |
surface | "editor" | "layer" | "interactive" | Which surface called this function |
ctx.fetch
An egress-restricted version of fetch(). Only URLs matching the extension's egress allowlist are permitted:
// Works if site.api.espn.com is in the allowlist
const res = await ctx.fetch("https://site.api.espn.com/...");
// Rejected: not in allowlist
const res = await ctx.fetch("https://malicious.example.com/..."); // throws
See External APIs for allowlist configuration.
ctx.secrets
Access secrets stored in the extension dashboard:
const apiKey = ctx.secrets.get("MY_API_KEY"); // returns string | null
See Secrets for management instructions.
Sandbox limits
Handler-based functions run in a V8 isolate with these constraints:
| Limit | Value |
|---|---|
| Memory | 128 MB per invocation |
| CPU timeout | 5 seconds |
| Response size | 4 MB |
ctx.fetch calls per invocation | 10 |
| Concurrent invocations per installation | 5 |
Exceeding any limit terminates the invocation and returns an error to the client.
Validators
Use v (from @zaflun/lumio-sdk/server) to type-check function arguments:
import { action, v } from "@zaflun/lumio-sdk/server";
export const example = action({
args: {
name: v.string(),
count: v.number(),
enabled: v.boolean(),
tags: v.array(v.string()),
config: v.object({ x: v.number(), y: v.number() }),
optionalNote: v.string().optional(),
},
handler: async (ctx, args) => {
// args is fully typed: { name: string, count: number, enabled: boolean, ... }
},
});
Arguments that do not match the declared validators are rejected before the handler runs.