/convex-best-practices
Guidelines for building production-ready Convex apps covering function organization, query patterns, validation, TypeScript usage, error handling, and the Zen of Convex design philosophy
$ npx -y skills add julianromli/ai-skills --skill convex-best-practices --agent claude-codeHow it fires
How this skill gets triggered: by you, by Claude, or both.
- Fires itselfAuto-invocation. Claude auto-loads it when your prompt matches the work.Auto-invocation is when the right skill fires by itself at the right moment, driven by a FLOW.md router and a hook, instead of you invoking it by name. It is the difference between a skill being installed and a skill actually getting used.Read the full definition →
- You can call itInvoke it directly when you want it.
- Slash command
/convex-best-practices
Context preview
The summary Claude sees to decide when to auto-load this skill.
Guidelines for building production-ready Convex apps covering function organization, query patterns, validation, TypeScript usage, error handling, and the Zen of Convex design philosophy
SKILL.md
convex-best-practices.SKILL.mdname: convex-best-practices
description: Guidelines for building production-ready Convex apps covering function organization, query patterns, validation, TypeScript usage, error handling, and the Zen of Convex design philosophy
Convex Best Practices
Build production-ready Convex applications by following established patterns for function organization, query optimization, validation, TypeScript usage, and error handling.
Code Quality
All patterns in this skill comply with `@convex-dev/eslint-plugin`. Install it for build-time validation:
npm i @convex-dev/eslint-plugin --save-dev
// eslint.config.js
import { defineConfig } from "eslint/config";
import convexPlugin from "@convex-dev/eslint-plugin";
export default defineConfig([
...convexPlugin.configs.recommended,
]);The plugin enforces four rules:
| Rule | What it enforces | | ----------------------------------- | --------------------------------- | | `no-old-registered-function-syntax` | Object syntax with `handler` | | `require-argument-validators` | `args: {}` on all functions | | `explicit-table-ids` | Table name in db operations | | `import-wrong-runtime` | No Node imports in Convex runtime |
Docs: https://docs.convex.dev/eslint
Documentation Sources
Before implementing, do not assume; fetch the latest documentation:
- Primary: https://docs.convex.dev/understanding/best-practices/
- Error Handling: https://docs.convex.dev/functions/error-handling
- Write Conflicts: https://docs.convex.dev/error#1
- For broader context: https://docs.convex.dev/llms.txt
Instructions
The Zen of Convex
1. **Convex manages the hard parts** - Let Convex handle caching, real-time sync, and consistency 2. **Functions are the API** - Design your functions as your application's interface 3. **Schema is truth** - Define your data model explicitly in schema.ts 4. **TypeScript everywhere** - Leverage end-to-end type safety 5. **Queries are reactive** - Think in terms of subscriptions, not requests
Function Organization
Organize your Convex functions by domain:
// convex/users.ts - User-related functions
import { query, mutation } from "./_generated/server";
import { v } from "convex/values";
export const get = query({
args: { userId: v.id("users") },
returns: v.union(
v.object({
_id: v.id("users"),
_creationTime: v.number(),
name: v.string(),
email: v.string(),
}),
v.null(),
),
handler: async (ctx, args) => {
return await ctx.db.get("users", args.userId);
},
});Argument and Return Validation
Always define validators for arguments AND return types:
export const createTask = mutation({
args: {
title: v.string(),
description: v.optional(v.string()),
priority: v.union(v.literal("low"), v.literal("medium"), v.literal("high")),
},
returns: v.id("tasks"),
handler: async (ctx, args) => {
return await ctx.db.insert("tasks", {
title: args.title,
description: args.description,
priority: args.priority,
completed: false,
createdAt: Date.now(),
});
},
});Query Patterns
Use indexes instead of filters for efficient queries:
// Schema with index
export default defineSchema({
tasks: defineTable({
userId: v.id("users"),
status: v.string(),
createdAt: v.number(),
})
.index("by_user", ["userId"])
.index("by_user_and_status", ["userId", "status"]),
});
// Query using index
export const getTasksByUser = query({
args: { userId: v.id("users") },
returns: v.array(
v.object({
_id: v.id("tasks"),
_creationTime: v.number(),
userId: v.id("users"),
status: v.string(),
createdAt: v.number(),
}),
),
handler: async (ctx, args) => {
return await ctx.db
.query("tasks")
.withIndex("by_user", (q) => q.eq("userId", args.userId))
.order("desc")
.collect();
},
});Error Handling
Use ConvexError for user-facing errors:
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;
},
});Avoiding Write Conflicts (Optimistic Concurrency Control)
Convex uses OCC. Follow these patterns to minimize conflicts:
// GOOD: Make mutations idempotent
export const completeTask = mutation({
args: { taskId: v.id("tasks") },
returns: v.null(),
handler: async (ctx, args) => {
const task = await ctx.db.get("tasks", args.taskId);
// Early return if already complete (idempotent)
if (!task || task.status === "completed") {
return null;
}
await ctx.db.patch("tasks", args.taskId, {
status: "completed",
completedAt: Date.now(),
});
return null;
},
});
// GOOD: Patch directly without reading first when possible
export const updateNote = mutation({
args: { id: v.id("notes"), content: v.string() },
returns: v.null(),
handler: async (ctx, args) => {
// Patch directly - ctx.db.patch throws if document doesn't exist
await ctx.db.patch("notes", args.id, { content: args.content });
return null;
},
});
// GOOD: Use Promise.all for parallel independent updates
export const reorderItems = mutation({
args: { itemIds: v.array(v.id("items")) },
returns: v.null(),
handler: async (ctx, args) => {
const updates = args.itemIds.map((id, index) =>
ctx.db.patch("items", id, { order: index }),
);
await Promise.all(updates);
return null;
},
});#
Read more
name: convex-best-practices description: Guidelines for building production-ready Convex apps covering function organization, query patterns, validation, TypeScript usage, error handling, and the Zen of Convex design philosophy
Convex Best Practices
Build production-ready Convex applications by following established patterns for function organization, query optimization, validation, TypeScript usage, and error handling.
Code Quality
All patterns in this skill comply with `@convex-dev/eslint-plugin`. Install it for build-time validation:
npm i @convex-dev/eslint-plugin --save-dev
// eslint.config.js
import { defineConfig } from "eslint/config";
import convexPlugin from "@convex-dev/eslint-plugin";
export default defineConfig([
...convexPlugin.configs.recommended,
]);The plugin enforces four rules:
| Rule | What it enforces | | ----------------------------------- | --------------------------------- | | `no-old-registered-function-syntax` | Object syntax with `handler` | | `require-argument-validators` | `args: {}` on all functions | | `explicit-table-ids` | Table name in db operations | | `import-wrong-runtime` | No Node imports in Convex runtime |
Docs: https://docs.convex.dev/eslint
Documentation Sources
Before implementing, do not assume; fetch the latest documentation:
- Primary: https://docs.convex.dev/understanding/best-practices/
- Error Handling: https://docs.convex.dev/functions/error-handling
- Write Conflicts: https://docs.convex.dev/error#1
- For broader context: https://docs.convex.dev/llms.txt
Instructions
The Zen of Convex
1. **Convex manages the hard parts** - Let Convex handle caching, real-time sync, and consistency 2. **Functions are the API** - Design your functions as your application's interface 3. **Schema is truth** - Define your data model explicitly in schema.ts 4. **TypeScript everywhere** - Leverage end-to-end type safety 5. **Queries are reactive** - Think in terms of subscriptions, not requests
Function Organization
Organize your Convex functions by domain:
// convex/users.ts - User-related functions
import { query, mutation } from "./_generated/server";
import { v } from "convex/values";
export const get = query({
args: { userId: v.id("users") },
returns: v.union(
v.object({
_id: v.id("users"),
_creationTime: v.number(),
name: v.string(),
email: v.string(),
}),
v.null(),
),
handler: async (ctx, args) => {
return await ctx.db.get("users", args.userId);
},
});Argument and Return Validation
Always define validators for arguments AND return types:
export const createTask = mutation({
args: {
title: v.string(),
description: v.optional(v.string()),
priority: v.union(v.literal("low"), v.literal("medium"), v.literal("high")),
},
returns: v.id("tasks"),
handler: async (ctx, args) => {
return await ctx.db.insert("tasks", {
title: args.title,
description: args.description,
priority: args.priority,
completed: false,
createdAt: Date.now(),
});
},
});Query Patterns
Use indexes instead of filters for efficient queries:
// Schema with index
export default defineSchema({
tasks: defineTable({
userId: v.id("users"),
status: v.string(),
createdAt: v.number(),
})
.index("by_user", ["userId"])
.index("by_user_and_status", ["userId", "status"]),
});
// Query using index
export const getTasksByUser = query({
args: { userId: v.id("users") },
returns: v.array(
v.object({
_id: v.id("tasks"),
_creationTime: v.number(),
userId: v.id("users"),
status: v.string(),
createdAt: v.number(),
}),
),
handler: async (ctx, args) => {
return await ctx.db
.query("tasks")
.withIndex("by_user", (q) => q.eq("userId", args.userId))
.order("desc")
.collect();
},
});Error Handling
Use ConvexError for user-facing errors:
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;
},
});Avoiding Write Conflicts (Optimistic Concurrency Control)
Convex uses OCC. Follow these patterns to minimize conflicts:
// GOOD: Make mutations idempotent
export const completeTask = mutation({
args: { taskId: v.id("tasks") },
returns: v.null(),
handler: async (ctx, args) => {
const task = await ctx.db.get("tasks", args.taskId);
// Early return if already complete (idempotent)
if (!task || task.status === "completed") {
return null;
}
await ctx.db.patch("tasks", args.taskId, {
status: "completed",
completedAt: Date.now(),
});
return null;
},
});
// GOOD: Patch directly without reading first when possible
export const updateNote = mutation({
args: { id: v.id("notes"), content: v.string() },
returns: v.null(),
handler: async (ctx, args) => {
// Patch directly - ctx.db.patch throws if document doesn't exist
await ctx.db.patch("notes", args.id, { content: args.content });
return null;
},
});
// GOOD: Use Promise.all for parallel independent updates
export const reorderItems = mutation({
args: { itemIds: v.array(v.id("items")) },
returns: v.null(),
handler: async (ctx, args) => {
const updates = args.itemIds.map((id, index) =>
ctx.db.patch("items", id, { order: index }),
);
await Promise.all(updates);
return null;
},
});#
A curated collection of AI agent skills for coding, frontend, debugging, product thinking, and workflow automation.
Repo: julianromli/ai-skills
Other skills on ai-skills.
- /agent-browser
Browser automation CLI for AI agents. Use when the user needs to interact with websites, including navigating pages, filling forms, clicking buttons, taking screenshots, extracting data, testing web apps, or automating any browser task. Triggers include requests to "open a
Open skill - /brainstorming
You MUST use this before any creative work - creating features, building components, adding functionality, or modifying behavior. Explores user intent, requirements and design before implementation.
Open skill - /clone-website
Reverse-engineer and clone one or more websites in one shot — extracts assets, CSS, and content section-by-section and proactively dispatches parallel builder agents in worktrees as it goes. Use this whenever the user wants to clone, replicate, rebuild, reverse-engineer, or copy
Open skill - /composition-patterns
Composition patterns for building flexible, maintainable React components. Avoid boolean prop proliferation by using compound components, lifting state, and composing internals. These patterns make codebases easier for both humans and AI agents to work with as they scale.
Open skill - /deploy-to-vercel
Deploy applications and websites to Vercel. Use when the user requests deployment actions like "deploy my app", "deploy and give me the link", "push this live", or "create a preview deployment".
Open skill - /frontend-design
Create distinctive, production-grade frontend interfaces with high design quality. Use this skill when the user asks to build web components, pages, artifacts, posters, or applications (examples include websites, landing pages, dashboards, React components, HTML/CSS layouts, or
Open skill

