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