Auth and Scopes
Server functions can inspect the identity of the caller and restrict access based on surface, role, or authentication status.
ctx.auth
Available in all handler-based functions (query(), mutation(), action()):
export const myMutation = mutation({
args: { text: v.string() },
handler: async (ctx, args) => {
const { userId, accountId, role, surface } = ctx.auth;
if (!userId) {
throw new Error("Authentication required");
}
if (role !== "owner" && role !== "editor") {
throw new Error("Only editors can perform this action");
}
// ... proceed
},
});
ctx.auth properties
| Property | Type | Description |
|---|---|---|
userId | string | null | Lumio user ID of the dashboard user, or null on the layer |
accountId | string | The Lumio account that owns this installation |
role | "owner" | "editor" | "viewer" | Caller's role in the account |
surface | "editor" | "layer" | Which surface made this request |
platformUserId | string | null | Platform user ID if viewer is connected |
platformUserName | string | null | Platform username if viewer is connected |
editorOnly for declarative functions
For declarative functions, use the editorOnly option instead of ctx.auth:
// Only the editor surface can call this
export const deleteRule = deleteRow("rules", arg("id"), { editorOnly: true });
// Any surface can call this
export const getRules = queryRows("rules");
editorOnly: true is equivalent to checking ctx.auth.surface === "editor" in a handler function. Use it when:
- The operation is destructive (delete, reset)
- The operation changes configuration (only the streamer/editor should control this)
- The overlay or widget running in OBS must not be able to trigger it
How Lumio decides the surface
Your extension cannot claim a surface. The editorOnly flag comes from the definition in your published functions.json, and the surface comes from the credential of the host that runs your extension:
| Host | Credential | Surface | editorOnly functions |
|---|---|---|---|
| Lumio dashboard (editor panel, previews) | the streamer's session | editor | allowed |
OBS overlay (/overlay/…) | the overlay's own token | layer | refused |
OBS widget (/widget/…) | the widget's own token | layer | refused |
A refused call rejects with Editor-only function called outside editor (JSON-RPC code DENIED, see useMutation).
Surface-based restrictions
In handler functions, use ctx.auth.surface to apply different logic per surface:
export const addItem = mutation({
args: { text: v.string() },
handler: async (ctx, args) => {
if (ctx.auth.surface === "layer") {
throw new Error("Cannot write from the layer surface");
}
await ctx.db.insert("items", {
text: args.text,
userId: ctx.auth.userId,
submittedFrom: ctx.auth.surface,
});
},
});
Viewers
Viewers never call server functions: Lumio has no viewer page and no viewer identity. Viewer input arrives as chat commands and channel point rewards, which a bot-module handler receives with the chat sender in ctx.user. See Viewer Input.
Role-based access
Use ctx.auth.role to restrict operations to specific account roles:
export const resetAll = mutation({
args: {},
handler: async (ctx) => {
if (ctx.auth.role !== "owner") {
throw new Error("Only the account owner can reset all data");
}
// Perform destructive reset...
},
});
Summary
| Restriction type | Mechanism |
|---|---|
| Editor surface only | { editorOnly: true } option on declarative functions |
| Surface check in handler | ctx.auth.surface === "editor" |
| Authentication required | if (!ctx.auth.userId) throw ... |
| Role check | if (ctx.auth.role !== "owner") throw ... |
| Platform identity | ctx.auth.platformUserId / ctx.auth.platformUserName |