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, or null for unauthenticated visitors |
accountId | string | The 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 |
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)
- 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 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 |