Skip to main content

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

PropertyTypeDescription
userIdstring | nullLumio user ID, or null for unauthenticated visitors
accountIdstringThe Lumio account that owns this installation
role"owner" | "editor" | "viewer" | "anonymous"Caller's role in the account
surface"editor" | "layer" | "interactive"Which surface made this request
platformUserIdstring | nullPlatform user ID if viewer is connected
platformUserNamestring | nullPlatform 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)
  • You want to prevent viewers from triggering writes through the interactive surface

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");
}

if (ctx.auth.surface === "interactive" && !ctx.auth.userId) {
throw new Error("Must be logged in to submit from the interactive page");
}

await ctx.db.insert("items", {
text: args.text,
userId: ctx.auth.userId,
submittedFrom: ctx.auth.surface,
});
},
});

Anonymous access on the interactive surface

The interactive surface allows unauthenticated visitors (they have a short-lived token but no Lumio account). Use ctx.auth.userId to differentiate:

export const castVote = mutation({
args: { choice: v.string() },
handler: async (ctx, args) => {
// Allow anonymous votes (token is sufficient for interactive surface)
// Use platformUserId as a fallback identifier
const voterId = ctx.auth.userId ?? ctx.auth.platformUserId ?? "anonymous";

await ctx.db.insert("votes", {
choice: args.choice,
voterId,
});
},
});

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 typeMechanism
Editor surface only{ editorOnly: true } option on declarative functions
Surface check in handlerctx.auth.surface === "editor"
Authentication requiredif (!ctx.auth.userId) throw ...
Role checkif (ctx.auth.role !== "owner") throw ...
Platform identityctx.auth.platformUserId / ctx.auth.platformUserName