Skip to main content

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 caseFunction type
Complex query with aggregationquery()
Conditional logic before writemutation()
External HTTP API callsaction()
Reading secretsaction()

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:

PropertyTypeDescription
userIdstring | nullLumio user ID, or null for unauthenticated
accountIdstringAccount 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:

LimitValue
Memory128 MB per invocation
CPU timeout5 seconds
Response size4 MB
ctx.fetch calls per invocation10
Concurrent invocations per installation5

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.