Guidelines for developing with Convex backend-as-a-service platform, covering queries, mutations, actions, and real-time data patterns
Install
npx skills add https://github.com/mindrally/skills --skill convexSKILL.md
Convex Development Guidelines
You are an expert in Convex backend development, TypeScript, and real-time data synchronization patterns.
General Development Specifications
Code Style and Structure
- Write concise TypeScript using functional declarations, iterators, and modules
- Use descriptive variable names with auxiliary verbs (e.g., isLoading, hasError)
- Structure code with exported components, subcomponents, helpers, and static types
- Use dash-case for directories with named exports
- Prefer interfaces over types; avoid enums in favor of union types
- Use functional components with declarative JSX patterns
Error Handling
- Handle errors early in functions with guard clauses
- Log errors appropriately for debugging
- Provide user-friendly error messages
- Use Zod for form validation
- Implement proper error boundaries in React components
UI Framework Integration
- Use Shadcn UI and Radix UI for component primitives
- Style with Tailwind CSS using responsive, mobile-first design
- Minimize useClient, useEffect, and useState usage
- Leverage React Server Components where applicable
- Use Suspense for loading states and dynamic loading for code splitting
Convex-Specific Patterns
Queries
Structure queries using the query constructor:
import { query } from "./_generated/server";
import { v } from "convex/values";
export const getItems = query({
args: {
status: v.optional(v.string()),
},
handler: async (ctx, args) => {
// ctx provides: db, storage, auth
const identity = await ctx.auth.getUserIdentity();
if (args.status) {
return await ctx.db
.query("items")
.withIndex("by_status", (q) => q.eq("status", args.status))
.collect();
}
return await ctx.db.query("items").collect();
},
});
Important: Prefer Convex indexes over filters for better performance. Define indexes in schema.ts using the .index() method, then query with .withIndex().
Mutations
Structure mutations for database writes:
import { mutation } from "./_generated/server";
import { v } from "convex/values";
export const createItem = mutation({
args: {
title: v.string(),
description: v.optional(v.string()),
},
handler: async (ctx, args) => {
const identity = await ctx.auth.getUserIdentity();
if (!identity) {
throw new Error("Not authenticated");
}
return await ctx.db.insert("items", {
title: args.title,
description: args.description,
userId: identity.subject,
createdAt: Date.now(),
});
},
});
Actions
Use actions for external API calls and side effects:
import { action } from "./_generated/server";
import { v } from "convex/values";
export const sendEmail = action({
args: {
to: v.string(),
subject: v.string(),
body: v.string(),
},
handler: async (ctx, args) => {
// Actions can call external APIs
const response = await fetch("https://api.email-service.com/send", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(args),
});
return response.ok;
},
});
Schema Definition with Indexes
import { defineSchema, defineTable } from "convex/server";
import { v } from "convex/values";
export default defineSchema({
items: defineTable({
title: v.string(),
description: v.optional(v.string()),
status: v.string(),
userId: v.string(),
createdAt: v.number(),
})
.index("by_status", ["status"])
.index("by_user", ["userId"])
.index("by_user_and_status", ["userId", "status"]),
});
HTTP Router
Define HTTP routes for webhooks and external integrations:
import { httpRouter } from "convex/server";
import { httpAction } from "./_generated/server";
const http = httpRouter();
http.route({
path: "/webhook",
method: "POST",
handler: httpAction(async (ctx, request) => {
const body = await request.json();
// Process webhook
return new Response(JSON.stringify({ success: true }), {
status: 200,
headers: { "Content-Type": "application/json" },
});
}),
});
export default http;
Scheduled Jobs
Implement cron jobs for recurring tasks:
import { cronJobs } from "convex/server";
import { internal } from "./_generated/api";
const crons = cronJobs();
// Run every hour
crons.interval(
"cleanup-old-items",
{ hours: 1 },
internal.tasks.cleanupOldItems
);
// Run at specific time (daily at midnight UTC)
crons.monthly(
"monthly-report",
{ day: 1, hourUTC: 0, minuteUTC: 0 },
internal.reports.generateMonthlyReport
);
export default crons;
File Handling
Three-step process for file uploads:
// 1. Generate upload URL (mutation)
export const generateUploadUrl = mutation(async (ctx) => {
return await ctx.storage.generateUploadUrl();
});
// 2. Client POSTs file to the URL
// const uploadUrl = await generateUploadUrl();
// const response = await fetch(uploadUrl, { method: "POST", body: file });
// const { storageId } = await response.json();
// 3. Save storage ID to database (mutation)
export const saveFile = mutation({
args: {
storageId: v.id("_storage"),
filename: v.string(),
},
handler: async (ctx, args) => {
return await ctx.db.insert("files", {
storageId: args.storageId,
filename: args.filename,
});
},
});
Best Practices
- Always use indexes for queries that filter or sort data
- Validate arguments using Convex validators (
v.string(),v.number(), etc.) - Check authentication early in handlers that require it
- Use internal functions for operations that should not be exposed to clients
- Leverage real-time subscriptions - Convex queries automatically update when data changes
- Keep mutations small and focused on single operations
- Use actions for side effects - never call external APIs from queries or mutations
- Handle errors gracefully with proper error messages for users
Performance Considerations
- Use
.withIndex()instead of.filter()whenever possible - Paginate large result sets using
.paginate() - Use
.first()instead of.collect()when expecting a single result - Consider data denormalization for frequently accessed data
- Use Convex's built-in caching - avoid implementing your own
Related skills
entra-app-registrationmicrosoft606KGuides Microsoft Entra ID app registration, OAuth 2.0 authentication, and MSAL integration. USE FOR: create app registration, register Azure AD app, configure OAuth, set up authentication, add API permissions, generate service principal, MSAL example, console app auth, Entra ID setup, Azure AD authentication. DO NOT USE FOR: Key Vault secrets (use azure-keyvault-expiration-audit), general Azure resource security guidance.azure-messagingmicrosoft595KTroubleshoot and resolve issues with Azure Messaging SDKs for Event Hubs and Service Bus. Covers connection failures, authentication errors, message processing issues, and SDK configuration problems. WHEN: event hub SDK error, service bus SDK issue, messaging connection failure, AMQP error, event processor host issue, message lock lost, message lock expired, lock renewal, lock renewal batch, send timeout, receiver disconnected, SDK troubleshooting, azure messaging SDK, event hub consumer, servicentra-agent-idmicrosoft328KProvision Microsoft Entra Agent Identity Blueprints, BlueprintPrincipals, and per-instance Agent Identities via Microsoft Graph, and configure OAuth 2.0 token exchange (fmi_path, OBO, cross-tenant) including the Microsoft Entra SDK for AgentID sidecar. USE FOR: Agent Identity Blueprint, BlueprintPrincipal, agent OAuth, fmi_path token exchange, agent OBO, Workload Identity Federation for agents, polyglot agent auth, Microsoft.Identity.Web.AgentIdentities. DO NOT USE FOR: standard Entra app registsupabasesupabase298KUse when doing ANY task involving Supabase. Triggers: Supabase products (Database, Auth, Edge Functions, Realtime, Storage, Vectors, Cron, Queues); client libraries and SSR integrations (supabase-js, @supabase/ssr) in Next.js, React, SvelteKit, Astro, Remix; auth issues (login, logout, sessions, JWT, cookies, getSession, getUser, getClaims, RLS); Supabase CLI or MCP server; schema changes, migrations, declarative schemas, security audits, Postgres extensions (pg_graphql, pg_cron, pg_vector); deb