Quick security audit checklist covering authentication, function exposure, argument validation, row-level access control, and environment variable handling
62
53%
Does it follow best practices?
Impact
Pending
No eval scenarios have been run
Passed
No known issues
Optimize this skill with Tessl
npx tessl skill review --optimize ./skills/convex-security-check/SKILL.mdA quick security audit checklist for Convex applications covering authentication, function exposure, argument validation, row-level access control, and environment variable handling.
Before implementing, do not assume; fetch the latest documentation:
Use this checklist to quickly audit your Convex application's security:
ctx.auth.getUserIdentity()query, mutation, action) reviewedinternalQuery, internalMutation, internalActionargs validatorsreturns validatorsv.any() used for sensitive data// convex/auth.ts
import { query, mutation } from "./_generated/server";
import { v } from "convex/values";
import { ConvexError } from "convex/values";
// Helper to require authentication
async function requireAuth(ctx: QueryCtx | MutationCtx) {
const identity = await ctx.auth.getUserIdentity();
if (!identity) {
throw new ConvexError("Authentication required");
}
return identity;
}
// Secure query pattern
export const getMyProfile = query({
args: {},
returns: v.union(v.object({
_id: v.id("users"),
name: v.string(),
email: v.string(),
}), v.null()),
handler: async (ctx) => {
const identity = await requireAuth(ctx);
return await ctx.db
.query("users")
.withIndex("by_tokenIdentifier", (q) =>
q.eq("tokenIdentifier", identity.tokenIdentifier)
)
.unique();
},
});// PUBLIC - Exposed to clients (review carefully!)
export const listPublicPosts = query({
args: {},
returns: v.array(v.object({ /* ... */ })),
handler: async (ctx) => {
// Anyone can call this - intentionally public
return await ctx.db
.query("posts")
.withIndex("by_public", (q) => q.eq("isPublic", true))
.collect();
},
});
// INTERNAL - Only callable from other Convex functions
export const _updateUserCredits = internalMutation({
args: { userId: v.id("users"), amount: v.number() },
returns: v.null(),
handler: async (ctx, args) => {
// This cannot be called directly from clients
await ctx.db.patch(args.userId, {
credits: args.amount,
});
return null;
},
});// GOOD: Strict validation
export const createPost = mutation({
args: {
title: v.string(),
content: v.string(),
category: v.union(
v.literal("tech"),
v.literal("news"),
v.literal("other")
),
},
returns: v.id("posts"),
handler: async (ctx, args) => {
const identity = await requireAuth(ctx);
return await ctx.db.insert("posts", {
...args,
authorId: identity.tokenIdentifier,
});
},
});
// BAD: Weak validation
export const createPostUnsafe = mutation({
args: {
data: v.any(), // DANGEROUS: Allows any data
},
returns: v.id("posts"),
handler: async (ctx, args) => {
return await ctx.db.insert("posts", args.data);
},
});// Verify ownership before update
export const updateTask = mutation({
args: {
taskId: v.id("tasks"),
title: v.string(),
},
returns: v.null(),
handler: async (ctx, args) => {
const identity = await requireAuth(ctx);
const task = await ctx.db.get(args.taskId);
// Check ownership
if (!task || task.userId !== identity.tokenIdentifier) {
throw new ConvexError("Not authorized to update this task");
}
await ctx.db.patch(args.taskId, { title: args.title });
return null;
},
});
// Verify ownership before delete
export const deleteTask = mutation({
args: { taskId: v.id("tasks") },
returns: v.null(),
handler: async (ctx, args) => {
const identity = await requireAuth(ctx);
const task = await ctx.db.get(args.taskId);
if (!task || task.userId !== identity.tokenIdentifier) {
throw new ConvexError("Not authorized to delete this task");
}
await ctx.db.delete(args.taskId);
return null;
},
});// convex/actions.ts
"use node";
import { action } from "./_generated/server";
import { v } from "convex/values";
export const sendEmail = action({
args: {
to: v.string(),
subject: v.string(),
body: v.string(),
},
returns: v.object({ success: v.boolean() }),
handler: async (ctx, args) => {
// Access API key from environment
const apiKey = process.env.RESEND_API_KEY;
if (!apiKey) {
throw new Error("RESEND_API_KEY not configured");
}
const response = await fetch("https://api.resend.com/emails", {
method: "POST",
headers: {
"Authorization": `Bearer ${apiKey}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
from: "noreply@example.com",
to: args.to,
subject: args.subject,
html: args.body,
}),
});
return { success: response.ok };
},
});// convex/secure.ts
import { query, mutation, internalMutation } from "./_generated/server";
import { v } from "convex/values";
import { ConvexError } from "convex/values";
// Authentication helper
async function getAuthenticatedUser(ctx: QueryCtx | MutationCtx) {
const identity = await ctx.auth.getUserIdentity();
if (!identity) {
throw new ConvexError({
code: "UNAUTHENTICATED",
message: "You must be logged in",
});
}
const user = await ctx.db
.query("users")
.withIndex("by_tokenIdentifier", (q) =>
q.eq("tokenIdentifier", identity.tokenIdentifier)
)
.unique();
if (!user) {
throw new ConvexError({
code: "USER_NOT_FOUND",
message: "User profile not found",
});
}
return user;
}
// Check admin role
async function requireAdmin(ctx: QueryCtx | MutationCtx) {
const user = await getAuthenticatedUser(ctx);
if (user.role !== "admin") {
throw new ConvexError({
code: "FORBIDDEN",
message: "Admin access required",
});
}
return user;
}
// Public: List own tasks
export const listMyTasks = query({
args: {},
returns: v.array(v.object({
_id: v.id("tasks"),
title: v.string(),
completed: v.boolean(),
})),
handler: async (ctx) => {
const user = await getAuthenticatedUser(ctx);
return await ctx.db
.query("tasks")
.withIndex("by_user", (q) => q.eq("userId", user._id))
.collect();
},
});
// Admin only: List all users
export const listAllUsers = query({
args: {},
returns: v.array(v.object({
_id: v.id("users"),
name: v.string(),
role: v.string(),
})),
handler: async (ctx) => {
await requireAdmin(ctx);
return await ctx.db.query("users").collect();
},
});
// Internal: Update user role (never exposed)
export const _setUserRole = internalMutation({
args: {
userId: v.id("users"),
role: v.union(v.literal("user"), v.literal("admin")),
},
returns: v.null(),
handler: async (ctx, args) => {
await ctx.db.patch(args.userId, { role: args.role });
return null;
},
});npx convex deploy unless explicitly instructed8ef49c9
If you maintain this skill, you can claim it as your own. Once claimed, you can manage eval scenarios, bundle related skills, attach documentation or rules, and ensure cross-agent compatibility.