convex-anti-patterns
Critical rules and common mistakes to avoid in Convex development. Use when reviewing Convex code, debugging issues, or learning what NOT to do. Activates for code review, debugging OCC errors, fixing type errors, or understanding why code fails in Convex.
What this skill does
# 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:**
```typescript
function handleData(data: any) { ... }
const items: any[] = [];
args: { data: v.any() } // Also avoid!
```
**✅ CORRECT:**
```typescript
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:**
```typescript
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:**
```typescript
// 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:**
```typescript
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:**
```typescript
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:**
```typescript
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:**
```typescript
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:**
```typescript
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:**
```typescript
// 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:**
```typescript
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:**
```typescript
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:**
```typescript
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:**
```typescript
// 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:**
```typescript
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:**
```typRelated in Code Review
gstack
IncludedFast headless browser for QA testing and site dogfooding. Navigate pages, interact with elements, verify state, diff before/after, take annotated screenshots, test responsive layouts, forms, uploads, dialogs, and capture bug evidence. Use when asked to open or test a site, verify a deployment, dogfood a user flow, or file a bug with screenshots. (gstack)
startup-due-diligence
IncludedLegal due diligence review for seed-stage and Series A startups (US, Delaware C-Corp focus). Supports both investor and founder perspectives. Capabilities include: (1) Interactive document review and issue spotting; (2) Document request list generation; (3) Cap table and SAFE/convertible note analysis; (4) Red flag identification with severity ratings; (5) Diligence report generation. TRIGGERS: due diligence, DD, startup investment, cap table review, Series A, seed round, investor diligence, legal review startup, SAFE analysis, convertible note, 409A, founder vesting.
interview-master
IncludedThis skill should be used when the user asks to "generate interview questions", "prepare for interview", "optimize resume", "conduct mock interview", "analyze git commits for resume", "generate resume from code", "review my resume", or mentions interview preparation, career assistance, or extracting project experience from git history. Provides comprehensive interview and career development guidance for both job seekers and interviewers.
fix-issue
IncludedFixes GitHub issues using parallel analysis agents for root cause investigation, code exploration, and regression detection. Reads issue context from gh CLI, searches codebase and memory for related patterns, generates a fix with tests, and links the resolution back to the issue via PR. Includes prevention analysis to avoid recurrence. Use when debugging errors, resolving regressions, fixing bugs, or triaging issues.
sf-apex
IncludedGenerates and reviews Salesforce Apex code with 150-point scoring. TRIGGER when: user writes, reviews, or fixes Apex classes, triggers, test classes, batch/queueable/schedulable jobs, or touches .cls/.trigger files. DO NOT TRIGGER when: LWC JavaScript (use sf-lwc), Flow XML (use sf-flow), SOQL-only queries (use sf-soql), or non-Salesforce code.
swift-development
IncludedComprehensive Swift development for building, testing, and deploying iOS/macOS applications. Use when Claude needs to: (1) Build Swift packages or Xcode projects from command line, (2) Run tests with XCTest or Swift Testing framework, (3) Manage iOS simulators with simctl, (4) Handle code signing, provisioning profiles, and app distribution, (5) Format or lint Swift code with SwiftFormat/SwiftLint, (6) Work with Swift Package Manager (SPM), (7) Implement Swift 6 concurrency patterns (async/await, actors, Sendable), (8) Create SwiftUI views with MVVM architecture, (9) Set up Core Data or SwiftData persistence, or any other Swift/iOS/macOS development tasks.