Convex Function Creator
Generate secure, type-safe Convex functions following all best practices.
When to Use
-
Creating new query functions (read data)
-
Creating new mutation functions (write data)
-
Creating new action functions (external APIs, long-running)
-
Adding API endpoints to your Convex backend
Function Types
Queries (Read-Only)
-
Can only read from database
-
Cannot modify data or call external APIs
-
Cached and reactive
-
Run in transactions
import { query } from "./_generated/server"; import { v } from "convex/values";
export const getTask = query({ args: { taskId: v.id("tasks") }, returns: v.union(v.object({ _id: v.id("tasks"), text: v.string(), completed: v.boolean(), }), v.null()), handler: async (ctx, args) => { return await ctx.db.get(args.taskId); }, });
Mutations (Transactional Writes)
-
Can read and write to database
-
Cannot call external APIs
-
Run in ACID transactions
-
Automatic retries on conflicts
import { mutation } from "./_generated/server"; import { v } from "convex/values";
export const createTask = mutation({ args: { text: v.string(), priority: v.optional(v.union( v.literal("low"), v.literal("medium"), v.literal("high") )), }, returns: v.id("tasks"), handler: async (ctx, args) => { const identity = await ctx.auth.getUserIdentity(); if (!identity) throw new Error("Not authenticated");
return await ctx.db.insert("tasks", {
text: args.text,
priority: args.priority ?? "medium",
completed: false,
createdAt: Date.now(),
});
}, });
Actions (External + Non-Transactional)
-
Can call external APIs (fetch, AI, etc.)
-
Can call mutations via ctx.runMutation
-
Cannot directly access database
-
No automatic retries
-
Use "use node" directive when needing Node.js APIs
Important: If your action needs Node.js-specific APIs (crypto, third-party SDKs, etc.), add "use node" at the top of the file. Files with "use node" can ONLY contain actions, not queries or mutations.
"use node"; // Required for Node.js APIs like OpenAI SDK
import { action } from "./_generated/server"; import { api } from "./_generated/api"; import { v } from "convex/values"; import OpenAI from "openai";
const openai = new OpenAI({ apiKey: process.env.OPENAI_API_KEY });
export const generateTaskSuggestion = action({ args: { prompt: v.string() }, returns: v.string(), handler: async (ctx, args) => { const identity = await ctx.auth.getUserIdentity(); if (!identity) throw new Error("Not authenticated");
// Call OpenAI (requires "use node")
const completion = await openai.chat.completions.create({
model: "gpt-4",
messages: [{ role: "user", content: args.prompt }],
});
const suggestion = completion.choices[0].message.content;
// Write to database via mutation
await ctx.runMutation(api.tasks.createTask, {
text: suggestion,
});
return suggestion;
}, });
Note: If you only need basic fetch (no Node.js APIs), you can omit "use node" . But for third-party SDKs, crypto, or other Node.js features, you must use it.
Required Components
- Argument Validation
Always define args with validators:
args: { id: v.id("tasks"), text: v.string(), count: v.number(), enabled: v.boolean(), tags: v.array(v.string()), metadata: v.optional(v.object({ key: v.string(), })), }
- Return Type Validation
Always define returns :
returns: v.object({ _id: v.id("tasks"), text: v.string(), })
// Or for arrays returns: v.array(v.object({ /* ... */ }))
// Or for nullable returns: v.union(v.object({ /* ... */ }), v.null())
- Authentication Check
Always verify auth in public functions:
const identity = await ctx.auth.getUserIdentity(); if (!identity) { throw new Error("Not authenticated"); }
- Authorization Check
Always verify ownership/permissions:
const task = await ctx.db.get(args.taskId); if (!task) { throw new Error("Task not found"); }
if (task.userId !== user._id) { throw new Error("Unauthorized"); }
Complete Examples
Secure Query with Auth
export const getMyTasks = query({ args: { status: v.optional(v.union( v.literal("active"), v.literal("completed") )), }, returns: v.array(v.object({ _id: v.id("tasks"), text: v.string(), completed: v.boolean(), })), handler: async (ctx, args) => { const identity = await ctx.auth.getUserIdentity(); if (!identity) throw new Error("Not authenticated");
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");
let query = ctx.db
.query("tasks")
.withIndex("by_user", q => q.eq("userId", user._id));
const tasks = await query.collect();
if (args.status) {
return tasks.filter(t =>
args.status === "completed" ? t.completed : !t.completed
);
}
return tasks;
}, });
Secure Mutation with Validation
export const updateTask = mutation({ args: { taskId: v.id("tasks"), text: v.optional(v.string()), completed: v.optional(v.boolean()), }, returns: v.id("tasks"), handler: async (ctx, args) => { // 1. Authentication const identity = await ctx.auth.getUserIdentity(); if (!identity) throw new Error("Not authenticated");
// 2. Get user
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");
// 3. Get resource
const task = await ctx.db.get(args.taskId);
if (!task) throw new Error("Task not found");
// 4. Authorization
if (task.userId !== user._id) {
throw new Error("Unauthorized");
}
// 5. Update
const updates: Partial<any> = {};
if (args.text !== undefined) updates.text = args.text;
if (args.completed !== undefined) updates.completed = args.completed;
await ctx.db.patch(args.taskId, updates);
return args.taskId;
}, });
Action Calling External API
Create separate file for actions that need Node.js:
// convex/taskActions.ts "use node"; // Required for SendGrid SDK
import { action } from "./_generated/server"; import { api } from "./_generated/api"; import { v } from "convex/values"; import sendgrid from "@sendgrid/mail";
sendgrid.setApiKey(process.env.SENDGRID_API_KEY);
export const sendTaskReminder = action({ args: { taskId: v.id("tasks") }, returns: v.boolean(), handler: async (ctx, args) => { // 1. Auth const identity = await ctx.auth.getUserIdentity(); if (!identity) throw new Error("Not authenticated");
// 2. Get data via query
const task = await ctx.runQuery(api.tasks.getTask, {
taskId: args.taskId,
});
if (!task) throw new Error("Task not found");
// 3. Call external service (using Node.js SDK)
await sendgrid.send({
to: identity.email,
from: "noreply@example.com",
subject: "Task Reminder",
text: `Don't forget: ${task.text}`,
});
// 4. Update via mutation
await ctx.runMutation(api.tasks.markReminderSent, {
taskId: args.taskId,
});
return true;
}, });
Note: Keep queries and mutations in convex/tasks.ts (without "use node"), and actions that need Node.js in convex/taskActions.ts (with "use node").
Internal Functions
For backend-only functions (called by scheduler, other functions):
import { internalMutation } from "./_generated/server";
export const processExpiredTasks = internalMutation({ args: {}, handler: async (ctx) => { // No auth needed - only callable from backend const now = Date.now(); const expired = await ctx.db .query("tasks") .withIndex("by_due_date", q => q.lt("dueDate", now)) .collect();
for (const task of expired) {
await ctx.db.patch(task._id, { status: "expired" });
}
}, });
Checklist
-
args defined with validators
-
returns defined with validator
-
Authentication check (ctx.auth.getUserIdentity() )
-
Authorization check (ownership/permissions)
-
All promises awaited
-
Indexed queries (no .filter() on queries)
-
Error handling with descriptive messages
-
Scheduled functions use internal.* not api.*
-
If using Node.js APIs: "use node" at top of file
-
If file has "use node" : Only actions (no queries/mutations)
-
Actions in separate file from queries/mutations when using "use node"