Awesome-omni-skill convex
Expert Convex development assistant. Use when users want to build full-stack TypeScript apps with Convex - including setup, server functions (queries/mutations/actions), database schema, auth, file storage, real-time features, frontend integration (React, Next.js, Vue), testing, or deployment.
git clone https://github.com/diegosouzapw/awesome-omni-skill
T=$(mktemp -d) && git clone --depth=1 https://github.com/diegosouzapw/awesome-omni-skill "$T" && mkdir -p ~/.claude/skills && cp -r "$T/skills/development/convex" ~/.claude/skills/diegosouzapw-awesome-omni-skill-convex-835770 && rm -rf "$T"
skills/development/convex/SKILL.mdThis skill provides expert guidance for building applications with Convex - the full-stack reactive database platform.
Quick Start
When a user wants to start with Convex:
-
New project:
npm create convex@latest npx convex dev -
Add to existing project:
npm install convex npx convex dev -
Verify: Check dashboard at https://dashboard.convex.dev
Core Concepts
Three Function Types
Queries - Read-only, auto-reactive:
import { query } from "./_generated/server"; export const list = query({ args: { channelId: v.id("channels") }, handler: async ({ db }, { channelId }) => { return await db.query("messages") .withIndex("by_channel", (q) => q.eq("channel", channelId)) .collect(); } });
Mutations - Write operations, transactions:
import { mutation } from "./_generated/server"; export const send = mutation({ args: { body: v.string(), channel: v.id("channels") }, handler: async ({ db }, { body, channel }) => { await db.insert("messages", { body, channel, createdAt: Date.now() }); } });
Actions - External API calls:
import { action } from "./_generated/server"; export const summarize = action({ args: { postId: v.id("posts") }, handler: async (ctx, { postId }) => { const post = await ctx.runQuery(api.posts.get, { postId }); const summary = await fetchLLM(post); await ctx.runMutation(api.posts.update, { postId, summary }); } });
Database Operations
// CRUD await db.insert("table", { field: value }); const doc = await db.get(id); await db.patch(id, { field: newValue }); await db.delete(id); // Query methods .collect() // All results .first() // First or null .unique() // Exactly one (throws if not) .paginate({ numItems: 50 }) // Pagination
Schema Definition
File:
convex/schema.ts
import { defineSchema, defineTable } from "convex/server"; import { v } from "convex/values"; export default defineSchema({ messages: defineTable({ body: v.string(), author: v.id("users"), channel: v.id("channels"), createdAt: v.number() }) .index("by_channel", ["channel"]) .index("by_channel_created", ["channel", "createdAt"]) });
Common types:
,v.string()
,v.number()v.boolean()
- Foreign keyv.id("table")
,v.array(T)v.object({})
,v.optional(T)v.union(...)
Frontend Integration
React
import { ConvexProvider, ConvexReactClient } from "convex/react"; const convex = new ConvexReactClient(process.env.NEXT_PUBLIC_CONVEX_URL!); <ConvexProvider client={convex}> <App /> </ConvexProvider>
Usage:
import { useQuery, useMutation } from "convex/react"; import { api } from "../convex/_generated/api"; const messages = useQuery(api.messages.list, { channelId }); const sendMessage = useMutation(api.messages.send);
Next.js App Router
Server-side preloading:
import { preloadQuery } from "convex/nextjs"; const preloaded = await preloadQuery(api.posts.list); return <ClientPage preloaded={preloaded} />;
Client component:
"use client"; import { usePreloadedQuery } from "convex/react"; const posts = usePreloadedQuery(preloaded);
Authentication
Clerk (Recommended)
Install:
npm install @clerk/clerk-react
File:
convex/auth.config.ts
import { AuthConfig } from "convex/server"; export default { providers: [{ domain: process.env.CLERK_JWT_ISSUER_DOMAIN!, applicationID: "convex" }] } satisfies AuthConfig;
Frontend:
import { ConvexProviderWithClerk } from "convex/react-clerk"; <ConvexProviderWithClerk client={convex} useAuth={useAuth}> <App /> </ConvexProviderWithClerk>
In functions:
const identity = await ctx.auth.getUserIdentity(); if (!identity) throw new Error("Unauthorized");
File Storage
// Upload const storageId = await ctx.storage.store(fileBlob); await db.insert("files", { storageId, name }); // Download const url = await ctx.storage.getUrl(storageId); // Delete await ctx.storage.delete(storageId);
HTTP Endpoints
File:
convex/http.ts
import { httpRouter } from "convex/server"; import { httpAction } from "./_generated/server"; const http = httpRouter(); http.route({ path: "/webhooks/stripe", method: "POST", handler: httpAction(async (ctx, request) => { const payload = await request.json(); await ctx.runMutation(internal.payments.process, { payload }); return new Response(null, { status: 200 }); }), }); export default http;
URL:
https://<deployment>.convex.site
Scheduled Tasks
Cron Jobs (File:
):convex/crons.ts
import { cronJobs } from "convex/server"; const crons = cronJobs(); crons.interval("cleanup", { hours: 1 }, internal.tasks.cleanup); crons.daily("digest", { hourUTC: 9 }, internal.notifications.send); export default crons;
Scheduler API (in mutations/actions):
// Schedule for later await ctx.scheduler.runAfter(delayMs, internal.tasks.process, { id }); // Schedule at specific time await ctx.scheduler.runAt(timestamp, internal.tasks.remind, { userId });
Best Practices
✅ DO
- Use indexes for queried fields
- Filter in application code, not with
on queries.filter() - Validate auth in protected functions
- Use pagination for large datasets
- Use
for structured errors clients can handleConvexError - Direct function calls, not multiple sequential
ctx.runQuery
❌ DON'T
- Use
on queries (use indexes or filter in code).filter() - Make multiple sequential
calls (consolidate)runQuery/runMutation - Forget to validate authentication
- Fetch entire tables without pagination
Error Handling
import { ConvexError } from "convex/values"; // Throw structured errors if (!identity) { throw new ConvexError({ code: "UNAUTHORIZED", message: "Login required" }); } // Client catches and handles if (error instanceof ConvexError) { console.error(error.data.code, error.data.message); }
Deployment
Production:
npx convex deploy
With frontend build:
npx convex deploy --cmd 'npm run build'
Environment: Set
CONVEX_DEPLOY_KEY for production
Testing
Install:
npm install convex-test vitest --save-dev
import { convexTest } from "convex-test"; const t = convexTest(schema); // Test mutations await t.mutation(api.posts.create, { title: "Test" }); // Test queries const posts = await t.query(api.posts.list); expect(posts).toHaveLength(1);
Common Issues
| Problem | Solution |
|---|---|
| Function not found | Run to regenerate types |
| Real-time not working | Check ConvexProvider wraps app |
| Type errors | Restart TypeScript server, regenerate types |
| Deploy fails | Verify is set |
Progressive Disclosure
Advanced topics in references:
- Complete setup: QUICK_START.md
- Deep dive on functions: CORE_CONCEPTS.md
- Auth patterns: AUTH.md
- Framework integration: FRONTEND.md
- Performance patterns: BEST_PRACTICES.md
- Production deployment: DEPLOYMENT.md
- Troubleshooting: TROUBLESHOOTING.md
- HTTP endpoints, cron, vector search, Convex Auth: ADVANCED.md
Your Approach
- Assess first: New project or existing? Framework? Goals?
- Start simple: Quick start → add complexity progressively
- Embrace reactivity: Leverage automatic real-time updates
- Type safety: Use TypeScript validation throughout
- Index for performance: Always use indexes on queried fields
- Validate auth: Check authentication in protected functions
- Test before deploying: Use convex-test for unit tests