convex
SKILL.md
Convex Development Skill
Convex is a reactive backend platform with TypeScript-first serverless functions, a real-time document database, and automatic caching.
Project Structure
my-app/
├── convex/ # Backend code (required)
│ ├── _generated/ # Auto-generated (don't edit)
│ │ ├── api.d.ts # Type-safe API references
│ │ ├── dataModel.d.ts # Database types from schema
│ │ └── server.d.ts # Function builders
│ ├── schema.ts # Database schema definition
│ ├── [feature].ts # Functions grouped by feature
│ └── convex.config.ts # Component configuration (optional)
├── .env.local # CONVEX_URL for dev deployment
└── package.json
CLI Commands
# Development
npx convex dev # Start dev server (syncs functions, generates types)
npx convex dev --once # Push once without watching
# Production
npx convex deploy # Deploy to production
npx convex deploy --cmd 'npm run build' # Deploy with build command
# Database
npx convex import --table <name> <file.jsonl> # Import data
npx convex export --path <dir> # Export backup
# Utilities
npx convex run <path:function> '{"arg": "value"}' # Run function
npx convex run --component <name> <path:function> # Run component function
npx convex env set <KEY> <value> # Set env variable
npx convex logs # Tail logs
npx convex login # Authenticate
Schema Definition
Define schema in convex/schema.ts:
import { defineSchema, defineTable } from "convex/server";
import { v } from "convex/values";
export default defineSchema({
users: defineTable({
name: v.string(),
email: v.string(),
role: v.union(v.literal("admin"), v.literal("user")),
metadata: v.optional(v.object({
avatar: v.optional(v.string()),
bio: v.optional(v.string()),
})),
})
.index("by_email", ["email"])
.index("by_role", ["role"]),
messages: defineTable({
userId: v.id("users"),
content: v.string(),
channelId: v.id("channels"),
})
.index("by_channel", ["channelId"])
.index("by_user_channel", ["userId", "channelId"]),
});
Validators Reference
v.string() // string
v.number() // number (float64)
v.int64() // 64-bit integer
v.boolean() // boolean
v.null() // null
v.id("tableName") // Document ID reference
v.array(v.string()) // Array
v.object({ ... }) // Object with specific shape
v.optional(v.string()) // Optional field
v.union(v.literal("a"), v.literal("b")) // Union type
v.any() // Any Convex value
v.bytes() // Binary data (ArrayBuffer)
Functions
Queries (Read-only, Reactive, Cached)
import { query } from "./_generated/server";
import { v } from "convex/values";
export const list = query({
args: { channelId: v.id("channels") },
returns: v.array(v.object({
_id: v.id("messages"),
content: v.string(),
author: v.string(),
})),
handler: async (ctx, args) => {
const messages = await ctx.db
.query("messages")
.withIndex("by_channel", (q) => q.eq("channelId", args.channelId))
.order("desc")
.take(50);
return Promise.all(messages.map(async (msg) => {
const user = await ctx.db.get(msg.userId);
return { _id: msg._id, content: msg.content, author: user?.name ?? "Unknown" };
}));
},
});
Mutations (Read/Write, Transactional)
import { mutation } from "./_generated/server";
import { v } from "convex/values";
export const send = mutation({
args: {
channelId: v.id("channels"),
content: v.string(),
},
returns: v.id("messages"),
handler: async (ctx, args) => {
const identity = await ctx.auth.getUserIdentity();
if (!identity) throw new Error("Unauthenticated");
const user = await ctx.db
.query("users")
.withIndex("by_token", (q) => q.eq("tokenIdentifier", identity.tokenIdentifier))
.unique();
if (!user) throw new Error("User not found");
return await ctx.db.insert("messages", {
userId: user._id,
channelId: args.channelId,
content: args.content,
});
},
});
Actions (Side Effects, External APIs)
import { action, internalMutation } from "./_generated/server";
import { internal } from "./_generated/api";
import { v } from "convex/values";
export const generateSummary = action({
args: { documentId: v.id("documents") },
returns: v.string(),
handler: async (ctx, args) => {
// Read data via query
const doc = await ctx.runQuery(internal.documents.get, { id: args.documentId });
// Call external API
const response = await fetch("https://api.openai.com/v1/chat/completions", {
method: "POST",
headers: {
"Authorization": `Bearer ${process.env.OPENAI_API_KEY}`,
"Content-Type": "application/json",
},
body: JSON.stringify({ /* ... */ }),
});
const result = await response.json();
// Write data via mutation
await ctx.runMutation(internal.documents.updateSummary, {
id: args.documentId,
summary: result.choices[0].message.content,
});
return result.choices[0].message.content;
},
});
Internal Functions
import { internalQuery, internalMutation, internalAction } from "./_generated/server";
// Only callable from other Convex functions, not from clients
export const adminGetUser = internalQuery({
args: { userId: v.id("users") },
handler: async (ctx, args) => {
return await ctx.db.get(args.userId);
},
});
Database Operations
// Insert
const id = await ctx.db.insert("messages", { content: "Hello", userId });
// Get by ID (returns null if not found)
const doc = await ctx.db.get(id);
// Update (partial)
await ctx.db.patch(id, { content: "Updated" });
// Replace (full document)
await ctx.db.replace(id, { content: "New", userId, channelId });
// Delete
await ctx.db.delete(id);
// Query with index
const results = await ctx.db
.query("messages")
.withIndex("by_channel", (q) =>
q.eq("channelId", channelId)
.gt("_creationTime", since)
)
.order("desc")
.take(100);
// Get unique result
const user = await ctx.db
.query("users")
.withIndex("by_email", (q) => q.eq("email", email))
.unique(); // Returns null or document, throws if multiple
React Integration
import { ConvexProvider, ConvexReactClient, useQuery, useMutation, useAction } from "convex/react";
import { api } from "../convex/_generated/api";
// Setup (main.tsx or _app.tsx)
const convex = new ConvexReactClient(import.meta.env.VITE_CONVEX_URL);
<ConvexProvider client={convex}><App /></ConvexProvider>
// In components
function Chat({ channelId }: { channelId: Id<"channels"> }) {
// Reactive query - auto-updates when data changes
const messages = useQuery(api.messages.list, { channelId });
// Mutation
const sendMessage = useMutation(api.messages.send);
// Action
const generateSummary = useAction(api.ai.generateSummary);
if (messages === undefined) return <Loading />;
return (
<div>
{messages.map(m => <Message key={m._id} {...m} />)}
<button onClick={() => sendMessage({ channelId, content: "Hi" })}>
Send
</button>
</div>
);
}
Scheduling
// Schedule for later
await ctx.scheduler.runAfter(60000, internal.emails.sendReminder, { userId });
await ctx.scheduler.runAt(timestamp, internal.tasks.process, { taskId });
// Cron jobs (convex/crons.ts)
import { cronJobs } from "convex/server";
import { internal } from "./_generated/api";
const crons = cronJobs();
crons.interval("cleanup", { minutes: 30 }, internal.maintenance.cleanup);
crons.cron("daily report", "0 9 * * *", internal.reports.daily);
export default crons;
File Storage
// Generate upload URL (mutation)
export const generateUploadUrl = mutation({
args: {},
handler: async (ctx) => {
return await ctx.storage.generateUploadUrl();
},
});
// Store file reference after upload
export const saveFile = mutation({
args: { storageId: v.id("_storage"), name: v.string() },
handler: async (ctx, args) => {
return await ctx.db.insert("files", {
storageId: args.storageId,
name: args.name,
});
},
});
// Get URL to serve file
export const getFileUrl = query({
args: { storageId: v.id("_storage") },
handler: async (ctx, args) => {
return await ctx.storage.getUrl(args.storageId);
},
});
HTTP Endpoints
// convex/http.ts
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();
await ctx.runMutation(internal.webhooks.process, { data: body });
return new Response(JSON.stringify({ success: true }), {
status: 200,
headers: { "Content-Type": "application/json" },
});
}),
});
export default http;
Best Practices
- Always validate arguments - Use
argswith validators on all functions - Use indexes for queries - Never use
.filter()on large datasets; use.withIndex() - Avoid
.collect()on unbounded queries - Use.take(n)or pagination - Await all promises - Missing awaits cause silent failures
- Keep queries/mutations fast - Target <100ms; use actions for heavy work
- Don't call actions from clients - Trigger via mutation → scheduler → action
- Use internal functions - Hide implementation details from client API
- Let queries handle reads - Don't return data from mutations for UI updates
Common Patterns
See references/PATTERNS.md for:
- Authentication patterns (Clerk, Auth0, Convex Auth)
- Pagination
- Optimistic updates
- Error handling
- Real-time subscriptions
- Components
Environment Variables
# Set in dashboard or CLI
npx convex env set OPENAI_API_KEY sk-xxx
# Access in actions (not queries/mutations in default runtime)
const apiKey = process.env.OPENAI_API_KEY;
# For Node.js runtime functions
// convex/myAction.ts
"use node";
import { action } from "./_generated/server";
Documentation Resources
- Full docs: https://docs.convex.dev
- Stack (patterns): https://stack.convex.dev
- Search: https://search.convex.dev
- Discord: https://convex.dev/community
Weekly Installs
2
Repository
derek-x-wang/skillsFirst Seen
Today
Security Audits
Installed on
windsurf2
amp2
cline2
opencode2
cursor2
kimi-cli2