Token导航 LogoToken导航TokenDH.com
前端设计external-servicegithub未标认证来源可访问clear审计通过

convex-anti-patterns凸反图案

Agent Skill

用于辅助前端页面、组件、样式和交互逻辑的开发与维护。它适合让 Agent 生成或审查 React、Next.js、Vue、Tailwind、CSS 等相关代码,整理组件结构,或定位布局和性能问题。使用时需要结合项目现有设计系统、路由和构建方式,避免只生成孤立片段;涉及页面改动时,应配合本地预览和构建检查确认视觉效果。

总安装

1,632

周安装

66

GitHub Stars

16

下载量

512
CodexClaudeCursorGemini CLI

安装说明

本站只整理中文说明和来源信息,不托管安装包,也不代用户安装。

GitHub

来源数

3

许可证

MIT

最后核验

2026-05-01

来源状态

来源可访问

安装方式

通过对话安装

复制提示词发给支持本地命令或 Skills 的 AI 助手,先确认命令和权限,再让它执行。

请帮我安装这个 Agent Skill:convex-anti-patterns(凸反图案)
来源仓库:https://github.com/fluid-tools/claude-skills
仓库路径:skills/convex-anti-patterns
安装命令:
npx skills add https://github.com/fluid-tools/claude-skills --skill convex-anti-patterns
安装前请先检查当前环境是否支持对应 CLI,并向我确认将要执行的命令、安装目录、联网范围和文件读写权限;确认后再执行。

命令行安装

复制命令到本机终端执行。不同来源提供的安装方式可能略有差异;本站展示可直接复制的安装命令,安装前请核对来源页面。

skills.shnpx skills
npx skills add https://github.com/fluid-tools/claude-skills --skill convex-anti-patterns

简介

记录 Convex 开发中的常见错误模式和 Agent 必须遵守的规则。

  • 帮助避免 TypeScript any 类型滥用、函数注册不规范等导致的生产问题。
  • 涵盖 schema 定义、索引命名、函数语法等核心开发规范要求。
  • 作为开发辅助工具,不能替代人工代码审查和安全审计流程。
  • convex-anti-patterns 属于前端设计类 Skill,可作为该场景下的辅助能力补充。

SKILL.md

Convex Anti-Patterns & Agent Rules

Overview

This skill documents critical mistakes to avoid in Convex development and rules that agents must follow. Every pattern here has caused real production issues.

TypeScript: NEVER Use any Type

CRITICAL RULE: This codebase has @typescript-eslint/no-explicit-any enabled. Using any will cause build failures.

❌ WRONG:

function handleData(data: any) { ... }
const items: any[] = [];
args: { data: v.any() }  // Also avoid!

✅ CORRECT:

function handleData(data: Doc<"items">) { ... }
const items: Doc<"items">[] = [];
args: { data: v.object({ field: v.string() }) }

When to Use This Skill

Use this skill when:

  • Reviewing Convex code for issues
  • Debugging mysterious errors
  • Understanding why code doesn't work as expected
  • Learning Convex best practices by counter-example
  • Checking code against known anti-patterns

Critical Anti-Patterns

Anti-Pattern 1: fetch() in Mutations

Mutations must be deterministic. External calls break this guarantee.

❌ WRONG:

export const createOrder = mutation({
  args: { productId: v.string() },
  returns: v.null(),
  handler: async (ctx, args) => {
    // ❌ Mutations cannot make external HTTP calls!
    const price = await fetch(
      `https://api.stripe.com/prices/${args.productId}`
    );
    await ctx.db.insert("orders", {
      productId: args.productId,
      price: await price.json(),
    });
    return null;
  },
});

✅ CORRECT:

// Mutation creates record, schedules action for external call
export const createOrder = mutation({
  args: { productId: v.string() },
  returns: v.id("orders"),
  handler: async (ctx, args) => {
    const orderId = await ctx.db.insert("orders", {
      productId: args.productId,
      status: "pending",
    });
    await ctx.scheduler.runAfter(0, internal.orders.fetchPrice, { orderId });
    return orderId;
  },
});

// Action handles external API call
export const fetchPrice = internalAction({
  args: { orderId: v.id("orders") },
  returns: v.null(),
  handler: async (ctx, args) => {
    const order = await ctx.runQuery(internal.orders.getById, {
      orderId: args.orderId,
    });
    if (!order) return null;

    const response = await fetch(
      `https://api.stripe.com/prices/${order.productId}`
    );
    const priceData = await response.json();

    await ctx.runMutation(internal.orders.updatePrice, {
      orderId: args.orderId,
      price: priceData.unit_amount,
    });
    return null;
  },
});

Anti-Pattern 2: ctx.db in Actions

Actions don't have database access. This is a common source of TypeScript errors.

❌ WRONG:

export const processData = action({
  args: { id: v.id("items") },
  returns: v.null(),
  handler: async (ctx, args) => {
    // ❌ Actions don't have ctx.db!
    const item = await ctx.db.get(args.id); // TypeScript Error!
    return null;
  },
});

✅ CORRECT:

export const processData = action({
  args: { id: v.id("items") },
  returns: v.null(),
  handler: async (ctx, args) => {
    // ✅ Use ctx.runQuery to read
    const item = await ctx.runQuery(internal.items.getById, { id: args.id });

    // Process with external APIs...
    const result = await fetch("https://api.example.com/process", {
      method: "POST",
      body: JSON.stringify(item),
    });

    // ✅ Use ctx.runMutation to write
    await ctx.runMutation(internal.items.updateResult, {
      id: args.id,
      result: await result.json(),
    });

    return null;
  },
});

Anti-Pattern 3: Missing returns Validator

Every function must have an explicit returns validator.

❌ WRONG:

export const doSomething = mutation({
  args: { data: v.string() },
  // ❌ Missing returns!
  handler: async (ctx, args) => {
    await ctx.db.insert("items", { data: args.data });
    // Implicitly returns undefined
  },
});

✅ CORRECT:

export const doSomething = mutation({
  args: { data: v.string() },
  returns: v.null(), // ✅ Explicit returns validator
  handler: async (ctx, args) => {
    await ctx.db.insert("items", { data: args.data });
    return null; // ✅ Explicit return value
  },
});

Anti-Pattern 4: Using.filter() on Queries

.filter() scans the entire table. Always use indexes.

❌ WRONG:

export const getActiveUsers = query({
  args: {},
  returns: v.array(v.object({ _id: v.id("users"), name: v.string() })),
  handler: async (ctx) => {
    // ❌ Full table scan!
    return await ctx.db
      .query("users")
      .filter((q) => q.eq(q.field("status"), "active"))
      .collect();
  },
});

✅ CORRECT:

// Schema: .index("by_status", ["status"])

export const getActiveUsers = query({
  args: {},
  returns: v.array(v.object({ _id: v.id("users"), name: v.string() })),
  handler: async (ctx) => {
    // ✅ Uses index
    return await ctx.db
      .query("users")
      .withIndex("by_status", (q) => q.eq("status", "active"))
      .collect();
  },
});

Anti-Pattern 5: Unbounded.collect()

Never collect without limits on potentially large tables.

❌ WRONG:

export const getAllMessages = query({
  args: { channelId: v.id("channels") },
  returns: v.array(v.object({ content: v.string() })),
  handler: async (ctx, args) => {
    // ❌ Could return millions of records!
    return await ctx.db
      .query("messages")
      .withIndex("by_channel", (q) => q.eq("channelId", args.channelId))
      .collect();
  },
});

✅ CORRECT:

export const getRecentMessages = query({
  args: { channelId: v.id("channels") },
  returns: v.array(v.object({ content: v.string() })),
  handler: async (ctx, args) => {
    // ✅ Bounded with take()
    return await ctx.db
      .query("messages")
      .withIndex("by_channel", (q) => q.eq("channelId", args.channelId))
      .order("desc")
      .take(50);
  },
});

Anti-Pattern 6:.collect().length for Counts

Collecting just to count is wasteful.

❌ WRONG:

export const getMessageCount = query({
  args: { channelId: v.id("channels") },
  returns: v.number(),
  handler: async (ctx, args) => {
    // ❌ Loads all messages just to count!
    const messages = await ctx.db
      .query("messages")
      .withIndex("by_channel", (q) => q.eq("channelId", args.channelId))
      .collect();
    return messages.length;
  },
});

✅ CORRECT:

// Option 1: Bounded count with "99+" display
export const getMessageCount = query({
  args: { channelId: v.id("channels") },
  returns: v.string(),
  handler: async (ctx, args) => {
    const messages = await ctx.db
      .query("messages")
      .withIndex("by_channel", (q) => q.eq("channelId", args.channelId))
      .take(100);
    return messages.length === 100 ? "99+" : String(messages.length);
  },
});

// Option 2: Denormalized counter (best for high traffic)
// Maintain messageCount field in channels table
export const getMessageCount = query({
  args: { channelId: v.id("channels") },
  returns: v.number(),
  handler: async (ctx, args) => {
    const channel = await ctx.db.get(args.channelId);
    return channel?.messageCount ?? 0;
  },
});

Anti-Pattern 7: N+1 Query Pattern

Loading related documents one by one.

❌ WRONG:

export const getPostsWithAuthors = query({
  args: {},
  returns: v.array(
    v.object({
      post: v.object({ title: v.string() }),
      author: v.object({ name: v.string() }),
    })
  ),
  handler: async (ctx) => {
    const posts = await ctx.db.query("posts").take(10);

    // ❌ N additional queries!
    const postsWithAuthors = await Promise.all(
      posts.map(async (post) => ({
        post: { title: post.title },
        author: await ctx.db
          .get(post.authorId)
          .then((a) => ({ name: a!.name })),
      }))
    );

    return postsWithAuthors;
  },
});

✅ CORRECT:

import { getAll } from "convex-helpers/server/relationships";

export const getPostsWithAuthors = query({
  args: {},
  returns: v.array(
    v.object({
      post: v.object({ title: v.string() }),
      author: v.union(v.object({ name: v.string() }), v.null()),
    })
  ),
  handler: async (ctx) => {
    const posts = await ctx.db.query("posts").take(10);

    // ✅ Batch fetch all authors
    const authorIds = [...new Set(posts.map((p) => p.authorId))];
    const authors = await getAll(ctx.db, authorIds);
    const authorMap = new Map(
      authors
        .filter((a): a is NonNullable<typeof a> => a !== null)
        .map((a) => [a._id, a])
    );

    return posts.map((post) => ({
      post: { title: post.title },
      author: authorMap.get(post.authorId)
        ? { name: authorMap.get(post.authorId)!.name }
        : null,
    }));
  },
});

Anti-Pattern 8: Global Counter (Hot Spot)

Single document updates cause OCC conflicts under load.

❌ WRONG:

export const incrementPageViews = mutation({
  args: {},
  returns: v.null(),
  handler: async (ctx) => {
    // ❌ Every request writes to same document!
    const stats = await ctx.db.query("globalStats").unique();
    await ctx.db.patch(stats!._id, { views: stats!.views + 1 });
    return null;
  },
});

✅ CORRECT:

// Option 1: Sharding
export const incrementPageViews = mutation({
  args: {},
  returns: v.null(),
  handler: async (ctx) => {
    // ✅ Write to random shard
    const shardId = Math.floor(Math.random() * 10);
    await ctx.db.insert("viewShards", { shardId, delta: 1 });
    return null;
  },
});

// Read by aggregating shards
export const getPageViews = query({
  args: {},
  returns: v.number(),
  handler: async (ctx) => {
    const shards = await ctx.db.query("viewShards").collect();
    return shards.reduce((sum, s) => sum + s.delta, 0);
  },
});

// Option 2: Use Workpool to serialize
import { Workpool } from "@convex-dev/workpool";

const counterPool = new Workpool(components.workpool, { maxParallelism: 1 });

export const incrementPageViews = mutation({
  args: {},
  returns: v.null(),
  handler: async (ctx) => {
    await counterPool.enqueueMutation(ctx, internal.stats.doIncrement, {});
    return null;
  },
});

Anti-Pattern 9: Using v.bigint() (Deprecated)

❌ WRONG:

export default defineSchema({
  counters: defineTable({
    value: v.bigint(), // ❌ Deprecated!
  }),
});

✅ CORRECT:

export default defineSchema({
  counters: defineTable({
    value: v.int64(), // ✅ Use v.int64()
  }),
});

Anti-Pattern 10: Missing System Fields in Return Validators

❌ WRONG:

export const getUser = query({
  args: { userId: v.id("users") },
  returns: v.object({
    // ❌ Missing _id and _creationTime!
    name: v.string(),
    email: v.string(),
  }),
  handler: async (ctx, args) => {
    return await ctx.db.get(args.userId); // Returns full doc including system fields
  },
});

✅ CORRECT:

export const getUser = query({
  args: { userId: v.id("users") },
  returns: v.union(
    v.object({
      _id: v.id("users"), // ✅ Include system fields
      _creationTime: v.number(),
      name: v.string(),
      email: v.string(),
    }),
    v.null()
  ),
  handler: async (ctx, args) => {
    return await ctx.db.get(args.userId);
  },
});

Anti-Pattern 11: Public Functions for Internal Logic

❌ WRONG:

// ❌ This is callable by any client!
export const deleteUserData = mutation({
  args: { userId: v.id("users") },
  returns: v.null(),
  handler: async (ctx, args) => {
    // Dangerous operation exposed publicly
    await ctx.db.delete(args.userId);
    return null;
  },
});

✅ CORRECT:

// Internal mutation - not callable by clients
export const deleteUserData = internalMutation({
  args: { userId: v.id("users") },
  returns: v.null(),
  handler: async (ctx, args) => {
    await ctx.db.delete(args.userId);
    return null;
  },
});

// Public mutation with auth check
export const requestAccountDeletion = mutation({
  args: {},
  returns: v.null(),
  handler: async (ctx) => {
    const identity = await ctx.auth.getUserIdentity();
    if (!identity) throw new Error("Unauthorized");

    const user = await ctx.db
      .query("users")
      .withIndex("by_token", (q) =>
        q.eq("tokenIdentifier", identity.tokenIdentifier)
      )
      .unique();

    if (!user) throw new Error("User not found");

    // Schedule internal mutation
    await ctx.scheduler.runAfter(0, internal.users.deleteUserData, {
      userId: user._id,
    });

    return null;
  },
});

Anti-Pattern 12: Non-Transactional Actions for Data Consistency

❌ WRONG:

export const transferFunds = action({
  args: { from: v.id("accounts"), to: v.id("accounts"), amount: v.number() },
  returns: v.null(),
  handler: async (ctx, args) => {
    // ❌ These are separate transactions - could leave inconsistent state!
    await ctx.runMutation(internal.accounts.debit, {
      accountId: args.from,
      amount: args.amount,
    });

    // If this fails, money was debited but not credited!
    await ctx.runMutation(internal.accounts.credit, {
      accountId: args.to,
      amount: args.amount,
    });

    return null;
  },
});

✅ CORRECT:

// Single atomic mutation
export const transferFunds = mutation({
  args: { from: v.id("accounts"), to: v.id("accounts"), amount: v.number() },
  returns: v.null(),
  handler: async (ctx, args) => {
    // ✅ All in one transaction - all succeed or all fail
    const fromAccount = await ctx.db.get(args.from);
    const toAccount = await ctx.db.get(args.to);

    if (!fromAccount || !toAccount) throw new Error("Account not found");
    if (fromAccount.balance < args.amount)
      throw new Error("Insufficient funds");

    await ctx.db.patch(args.from, {
      balance: fromAccount.balance - args.amount,
    });
    await ctx.db.patch(args.to, { balance: toAccount.balance + args.amount });

    return null;
  },
});

Anti-Pattern 13: Redundant Indexes

❌ WRONG:

export default defineSchema({
  messages: defineTable({
    channelId: v.id("channels"),
    authorId: v.id("users"),
    content: v.string(),
  })
    .index("by_channel", ["channelId"]) // ❌ Redundant!
    .index("by_channel_author", ["channelId", "authorId"]),
});

✅ CORRECT:

export default defineSchema({
  messages: defineTable({
    channelId: v.id("channels"),
    authorId: v.id("users"),
    content: v.string(),
  })
    // ✅ Single compound index serves both query patterns
    .index("by_channel_author", ["channelId", "authorId"]),
});

// Use prefix matching for channel-only queries:
// .withIndex("by_channel_author", (q) => q.eq("channelId", id))

Anti-Pattern 14: Using v.string() for IDs

❌ WRONG:

export const getMessage = query({
  args: { messageId: v.string() }, // ❌ Should be v.id()
  returns: v.null(),
  handler: async (ctx, args) => {
    // Type error or runtime error
    return await ctx.db.get(args.messageId as Id<"messages">);
  },
});

✅ CORRECT:

export const getMessage = query({
  args: { messageId: v.id("messages") }, // ✅ Proper ID type
  returns: v.union(
    v.object({
      _id: v.id("messages"),
      _creationTime: v.number(),
      content: v.string(),
    }),
    v.null()
  ),
  handler: async (ctx, args) => {
    return await ctx.db.get(args.messageId);
  },
});

Anti-Pattern 15: Retry Without Backoff or Jitter

❌ WRONG:

export const processWithRetry = internalAction({
  args: { jobId: v.id("jobs"), attempt: v.number() },
  returns: v.null(),
  handler: async (ctx, args) => {
    try {
      // Process...
    } catch (error) {
      if (args.attempt < 5) {
        // ❌ Fixed delay causes thundering herd!
        await ctx.scheduler.runAfter(5000, internal.jobs.processWithRetry, {
          jobId: args.jobId,
          attempt: args.attempt + 1,
        });
      }
    }
    return null;
  },
});

✅ CORRECT:

export const processWithRetry = internalAction({
  args: { jobId: v.id("jobs"), attempt: v.number() },
  returns: v.null(),
  handler: async (ctx, args) => {
    try {
      // Process...
    } catch (error) {
      if (args.attempt < 5) {
        // ✅ Exponential backoff + jitter
        const baseDelay = Math.pow(2, args.attempt) * 1000;
        const jitter = Math.random() * 1000;
        await ctx.scheduler.runAfter(
          baseDelay + jitter,
          internal.jobs.processWithRetry,
          {
            jobId: args.jobId,
            attempt: args.attempt + 1,
          }
        );
      }
    }
    return null;
  },
});

Agent Rules Summary

Must Do

  1. Always include returns validator on every function
  2. Always use indexes instead of .filter()
  3. Always use take(n) for potentially large queries
  4. Always use v.id("table") for document ID arguments
  5. Always use internalMutation/internalAction for sensitive operations
  6. Always handle errors in actions and update status in database
  7. Always use exponential backoff with jitter for retries

Must Not Do

  1. Never call fetch() in mutations
  2. Never access ctx.db in actions
  3. Never use .filter() on database queries
  4. Never use .collect() without limits on large tables
  5. Never use v.bigint() (deprecated, use v.int64())
  6. Never use any type (ESLint rule enforced)
  7. Never write to hot-spot documents without sharding/workpool
  8. Never expose dangerous operations as public functions
  9. Never rely on multiple mutations for atomic operations

Quick Checklist

Before submitting Convex code, verify:

  • All functions have returns validators
  • All queries use indexes (no .filter())
  • All .collect() calls are bounded with .take(n)
  • All ID arguments use v.id("tableName")
  • External API calls are in actions, not mutations
  • Actions use ctx.runQuery/ctx.runMutation for DB access
  • Sensitive operations use internal functions
  • No any types in the codebase
  • High-write documents use sharding or Workpool
  • Retries use exponential backoff with jitter

适合场景

01

用户想查找某类 Agent Skill 时

02

需要根据任务场景推荐可安装能力包时

03

需要对比不同来源的安装命令和来源信息时

04

需要参考平台分布和安装热度时

能力概览

能力 1

按任务关键词查找相关 Skills

能力 2

展示可复制的安装命令

能力 3

保留来源站点、仓库和原始说明,方便继续核验

能力 4

补充不同宿主或平台的使用分布数据

能力 5

展示第三方安全扫描或审计结果

安装后应在对应宿主中按原始 README 的触发条件使用;具体调用方式请以来源页面和 README 为准。

平台分布

Claude Code

30.48%
按下载量换算156

OpenCode

26.02%
按下载量换算133

Cursor

16.74%
按下载量换算86

Gemini CLI

11.83%
按下载量换算61

Antigravity

8.87%
按下载量换算45

Codex

3.35%
按下载量换算17

安全审计

Gen Agent Trust Hub

通过

Socket

通过

Snyk

通过

权限和风险

external-service

该 Skill 可能调用第三方服务、云服务或外部模型 API,使用前需要确认账号、额度、数据发送范围和服务条款。

安装前确认

本站仅展示第三方公开信息,不托管安装包,不提供自动安装或运行环境。安装前应自行审查源码、依赖和命令行为。

来源信息

继续浏览同类 Skills