Agent SkillsNeverSight/learn-skills.dev › convex-security

convex-security

GitHub

提供Convex函数安全最佳实践,涵盖错误处理、参数校验、认证授权及访问控制。用于编写或审查涉及身份验证、权限管理及数据安全的查询与变更函数。

data/skills-md/aaronvanston/skills-convex/convex-security/SKILL.md NeverSight/learn-skills.dev

Trigger Scenarios

编写Convex函数时添加安全逻辑 实现用户认证和角色权限检查 审查代码以增强安全性 配置行级访问控制

Install

npx skills add NeverSight/learn-skills.dev --skill convex-security -g -y
More Options

Non-standard path

npx skills add https://github.com/NeverSight/learn-skills.dev/tree/main/data/skills-md/aaronvanston/skills-convex/convex-security -g -y

Use without installing

npx skills use NeverSight/learn-skills.dev@convex-security

指定 Agent (Claude Code)

npx skills add NeverSight/learn-skills.dev --skill convex-security -a claude-code -g -y

安装 repo 全部 skill

npx skills add NeverSight/learn-skills.dev --all -g -y

预览 repo 内 skill

npx skills add NeverSight/learn-skills.dev --list

SKILL.md

Frontmatter
{
    "name": "convex-security",
    "description": "Security best practices for Convex functions including ConvexError handling, argument\/return validation, authentication helpers, access control, rate limiting, and internal functions. Use when writing public queries\/mutations\/actions, implementing authentication, adding authorization checks, handling errors, or reviewing Convex functions for security."
}

Convex Security

Error Handling with ConvexError

Use ConvexError for user-facing errors with structured error codes:

import { ConvexError } from "convex/values";

export const updateTask = mutation({
  args: { taskId: v.id("tasks"), title: v.string() },
  returns: v.null(),
  handler: async (ctx, args) => {
    const task = await ctx.db.get("tasks", args.taskId);

    if (!task) {
      throw new ConvexError({
        code: "NOT_FOUND",
        message: "Task not found",
      });
    }

    await ctx.db.patch("tasks", args.taskId, { title: args.title });
    return null;
  },
});

Common error codes: UNAUTHENTICATED, FORBIDDEN, NOT_FOUND, RATE_LIMITED, VALIDATION_ERROR

Argument AND Return Validators

Always define both args and returns validators:

export const createTask = mutation({
  args: {
    title: v.string(),
    priority: v.union(v.literal("low"), v.literal("medium"), v.literal("high")),
  },
  returns: v.id("tasks"),  // Always include returns!
  handler: async (ctx, args) => {
    return await ctx.db.insert("tasks", {
      title: args.title,
      priority: args.priority,
      completed: false,
    });
  },
});

ESLint: Use @convex-dev/require-argument-validators rule.

Authentication Helpers

Create reusable auth helpers:

// convex/lib/auth.ts
import { QueryCtx, MutationCtx } from "./_generated/server";
import { ConvexError } from "convex/values";
import { Doc } from "./_generated/dataModel";

export async function getUser(ctx: QueryCtx | MutationCtx): Promise<Doc<"users"> | null> {
  const identity = await ctx.auth.getUserIdentity();
  if (!identity) return null;

  return await ctx.db
    .query("users")
    .withIndex("by_tokenIdentifier", (q) =>
      q.eq("tokenIdentifier", identity.tokenIdentifier)
    )
    .unique();
}

export async function requireAuth(ctx: QueryCtx | MutationCtx): Promise<Doc<"users">> {
  const user = await getUser(ctx);
  if (!user) {
    throw new ConvexError({
      code: "UNAUTHENTICATED",
      message: "Authentication required",
    });
  }
  return user;
}

type UserRole = "user" | "moderator" | "admin";
const roleHierarchy: Record<UserRole, number> = { user: 0, moderator: 1, admin: 2 };

export async function requireRole(
  ctx: QueryCtx | MutationCtx,
  minRole: UserRole
): Promise<Doc<"users">> {
  const user = await requireAuth(ctx);
  const userLevel = roleHierarchy[user.role as UserRole] ?? 0;

  if (userLevel < roleHierarchy[minRole]) {
    throw new ConvexError({
      code: "FORBIDDEN",
      message: `Role '${minRole}' or higher required`,
    });
  }
  return user;
}

Usage:

export const adminOnly = mutation({
  args: {},
  returns: v.null(),
  handler: async (ctx) => {
    await requireRole(ctx, "admin");
    // ... admin logic
    return null;
  },
});

Row-Level Access Control

Verify ownership before operations:

export const updateTask = mutation({
  args: { taskId: v.id("tasks"), title: v.string() },
  returns: v.null(),
  handler: async (ctx, args) => {
    const user = await requireAuth(ctx);
    const task = await ctx.db.get("tasks", args.taskId);

    if (!task || task.userId !== user._id) {
      throw new ConvexError({ code: "FORBIDDEN", message: "Not authorized" });
    }

    await ctx.db.patch("tasks", args.taskId, { title: args.title });
    return null;
  },
});

Rate Limiting

Prevent abuse with rate limiting:

const RATE_LIMITS = {
  message: { requests: 10, windowMs: 60000 },  // 10 per minute
  upload: { requests: 5, windowMs: 300000 },   // 5 per 5 minutes
};

export const checkRateLimit = mutation({
  args: { userId: v.string(), action: v.string() },
  returns: v.object({ allowed: v.boolean(), retryAfter: v.optional(v.number()) }),
  handler: async (ctx, args) => {
    const limit = RATE_LIMITS[args.action];
    const windowStart = Date.now() - limit.windowMs;

    const requests = await ctx.db
      .query("rateLimits")
      .withIndex("by_user_and_action", (q) =>
        q.eq("userId", args.userId).eq("action", args.action)
      )
      .filter((q) => q.gt(q.field("timestamp"), windowStart))
      .collect();

    if (requests.length >= limit.requests) {
      return { allowed: false, retryAfter: requests[0].timestamp + limit.windowMs - Date.now() };
    }

    await ctx.db.insert("rateLimits", {
      userId: args.userId,
      action: args.action,
      timestamp: Date.now(),
    });
    return { allowed: true };
  },
});

Internal Functions Only for Scheduling

Always use internal.* for ctx.run* and scheduling:

// Bad - exposes public function
await ctx.scheduler.runAfter(0, api.tasks.process, { id });

// Good - uses internal function
await ctx.scheduler.runAfter(0, internal.tasks.process, { id });

// Internal function definition
export const process = internalMutation({
  args: { id: v.id("tasks") },
  returns: v.null(),
  handler: async (ctx, args) => {
    // ... processing logic
    return null;
  },
});

Tip: Never import api from _generated/api.ts in Convex functions.

Include Table Name in ctx.db

Always include table name as first argument:

// Bad
await ctx.db.get(movieId);
await ctx.db.patch(movieId, { title: "Whiplash" });

// Good
await ctx.db.get("movies", movieId);
await ctx.db.patch("movies", movieId, { title: "Whiplash" });

ESLint: Use @convex-dev/explicit-table-ids rule with autofix.

References

Version History

  • e0220ca Current 2026-07-05 22:13

Same Skill Collection

data/skills-md/00prabalk00/claude-skills/knowledge-base-gap-finder/SKILL.md
data/skills-md/01000001-01001110/agent-jira-skills/jira-agile/SKILL.md
data/skills-md/01000001-01001110/agent-jira-skills/jira-auth/SKILL.md
data/skills-md/01000001-01001110/agent-jira-skills/jira-issues/SKILL.md
data/skills-md/01000001-01001110/agent-jira-skills/jira-project-management/SKILL.md
data/skills-md/01000001-01001110/agent-jira-skills/jira-projects/SKILL.md
data/skills-md/01000001-01001110/agent-jira-skills/jira-safe/SKILL.md
data/skills-md/01000001-01001110/agent-jira-skills/jira-search/SKILL.md
data/skills-md/01000001-01001110/agent-jira-skills/jira-spaces/SKILL.md
data/skills-md/01000001-01001110/agent-jira-skills/jira-transitions/SKILL.md
data/skills-md/0731coderlee-sudo/wechat-publisher/wechat-publisher/SKILL.md
data/skills-md/0froq/skills/conventionalcommits/SKILL.md
data/skills-md/0froq/skills/nuxt/SKILL.md
data/skills-md/0froq/skills/oq/SKILL.md
data/skills-md/0froq/skills/pinia/SKILL.md
data/skills-md/0froq/skills/pnpm/SKILL.md
data/skills-md/0froq/skills/slidev/SKILL.md
data/skills-md/0froq/skills/tsdown/SKILL.md
data/skills-md/0froq/skills/turborepo/SKILL.md
data/skills-md/0froq/skills/unocss/SKILL.md
data/skills-md/0froq/skills/vitepress/SKILL.md
data/skills-md/0froq/skills/vitest/SKILL.md
data/skills-md/0froq/skills/vue-best-practices/SKILL.md
data/skills-md/0froq/skills/vue-router-best-practices/SKILL.md
data/skills-md/0froq/skills/vue-testing-best-practices/SKILL.md
data/skills-md/0froq/skills/vue/SKILL.md
data/skills-md/0froq/skills/vueuse-functions/SKILL.md
data/skills-md/0froq/skills/web-design-guidelines/SKILL.md
data/skills-md/0juano/agent-skills/bondterminal-x402/SKILL.md
data/skills-md/0juano/agent-skills/edgeone-pages-deploy/SKILL.md
data/skills-md/0juano/agent-skills/ley-ar/SKILL.md
data/skills-md/0juano/agent-skills/ticktick/SKILL.md
data/skills-md/0juano/agent-skills/x-image-cards/SKILL.md
data/skills-md/0juano/x-image-cards/x-image-cards/SKILL.md
data/skills-md/0x0funky/agent-sprite-forge/generate2dsprite/SKILL.md
data/skills-md/0x0funky/agent-sprite-forge/video2dsprite/SKILL.md
data/skills-md/0x2e/superpowers/brainstorming/SKILL.md
data/skills-md/0x2e/superpowers/dispatching-parallel-agents/SKILL.md
data/skills-md/0x2e/superpowers/executing-plans/SKILL.md
data/skills-md/0x2e/superpowers/finishing-a-development-branch/SKILL.md
data/skills-md/0x2e/superpowers/receiving-code-review/SKILL.md
data/skills-md/0x2e/superpowers/requesting-code-review/SKILL.md
data/skills-md/0x2e/superpowers/subagent-driven-development/SKILL.md
data/skills-md/0x2e/superpowers/systematic-debugging/SKILL.md
data/skills-md/0x2e/superpowers/test-driven-development/SKILL.md
data/skills-md/0x2e/superpowers/using-git-worktrees/SKILL.md
data/skills-md/0x2e/superpowers/using-superpowers/SKILL.md
data/skills-md/0x2e/superpowers/verification-before-completion/SKILL.md
data/skills-md/0x2e/superpowers/writing-plans/SKILL.md
data/skills-md/0x2e/superpowers/writing-skills/SKILL.md

Metadata

Files
0
Version
f4b4cf1
Hash
3678e2e5
Indexed
2026-07-05 22:13

Accueil - Wiki
Copyright © 2011-2026 iteam. Current version is 2.155.2. UTC+08:00, 2026-08-07 15:50
浙ICP备14020137号-1 $Carte des visiteurs$