skills/convex-best-practices/SKILL.md
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 skillsauth add julianromli/ai-skills convex-best-practicesInstall this skill globally with one command. Works with Claude Code, Cursor, and Windsurf.
3 of 9 scanners reported clean
Some scanners were skipped, did not run, or reported a non-clean status. Review each row below.
Build production-ready Convex applications by following established patterns for function organization, query optimization, validation, TypeScript usage, and error handling.
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
Before implementing, do not assume; fetch the latest documentation:
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);
},
});
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(),
});
},
});
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();
},
});
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;
},
});
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;
},
});
import { Id, Doc } from "./_generated/dataModel";
// Use Id type for document references
type UserId = Id<"users">;
// Use Doc type for full documents
type User = Doc<"users">;
// Define Record types properly
const userScores: Record<Id<"users">, number> = {};
// Public function - exposed to clients
export const getUser = query({
args: { userId: v.id("users") },
returns: v.union(
v.null(),
v.object({
/* ... */
}),
),
handler: async (ctx, args) => {
// ...
},
});
// Internal function - only callable from other Convex functions
export const _updateUserStats = internalMutation({
args: { userId: v.id("users") },
returns: v.null(),
handler: async (ctx, args) => {
// ...
},
});
// convex/tasks.ts
import { query, mutation } from "./_generated/server";
import { v } from "convex/values";
import { ConvexError } from "convex/values";
const taskValidator = v.object({
_id: v.id("tasks"),
_creationTime: v.number(),
title: v.string(),
completed: v.boolean(),
userId: v.id("users"),
});
export const list = query({
args: { userId: v.id("users") },
returns: v.array(taskValidator),
handler: async (ctx, args) => {
return await ctx.db
.query("tasks")
.withIndex("by_user", (q) => q.eq("userId", args.userId))
.collect();
},
});
export const create = mutation({
args: {
title: v.string(),
userId: v.id("users"),
},
returns: v.id("tasks"),
handler: async (ctx, args) => {
return await ctx.db.insert("tasks", {
title: args.title,
completed: false,
userId: args.userId,
});
},
});
export const update = mutation({
args: {
taskId: v.id("tasks"),
title: v.optional(v.string()),
completed: v.optional(v.boolean()),
},
returns: v.null(),
handler: async (ctx, args) => {
const { taskId, ...updates } = args;
// Remove undefined values
const cleanUpdates = Object.fromEntries(
Object.entries(updates).filter(([_, v]) => v !== undefined),
);
if (Object.keys(cleanUpdates).length > 0) {
await ctx.db.patch("tasks", taskId, cleanUpdates);
}
return null;
},
});
export const remove = mutation({
args: { taskId: v.id("tasks") },
returns: v.null(),
handler: async (ctx, args) => {
await ctx.db.delete("tasks", args.taskId);
return null;
},
});
npx convex deploy unless explicitly instructeddevelopment
Review UI code for Web Interface Guidelines compliance. Use when asked to "review my UI", "check accessibility", "audit design", "review UX", or "check my site against best practices".
tools
UI/UX design intelligence for web and mobile. Includes 50+ styles, 161 color palettes, 57 font pairings, 161 product types, 99 UX guidelines, and 25 chart types across 10 stacks (React, Next.js, Vue, Svelte, SwiftUI, React Native, Flutter, Tailwind, shadcn/ui, and HTML/CSS). Actions: plan, build, create, design, implement, review, fix, improve, optimize, enhance, refactor, and check UI/UX code. Projects: website, landing page, dashboard, admin panel, e-commerce, SaaS, portfolio, blog, and mobile app. Elements: button, modal, navbar, sidebar, card, table, form, and chart. Styles: glassmorphism, claymorphism, minimalism, brutalism, neumorphism, bento grid, dark mode, responsive, skeuomorphism, and flat design. Topics: color systems, accessibility, animation, layout, typography, font pairing, spacing, interaction states, shadow, and gradient. Integrations: shadcn/ui MCP for component search and examples.
development
Use when encountering any bug, test failure, or unexpected behavior, before proposing fixes
data-ai
Postgres performance optimization and best practices from Supabase. Use this skill when writing, reviewing, or optimizing Postgres queries, schema designs, or database configurations.