function query<ArgsValidator, Output>(func: {
args?: ArgsValidator;
handler: (ctx: QueryCtx, args: ObjectType<ArgsValidator>) => Promise<Output>;
}): RegisteredQuery<'public', ArgsValidator, Output>;
interface QueryCtx {
db: GenericDatabaseReader; // Read-only
auth: Auth;
storage: StorageReader;
runQuery<Q extends FunctionReference<'query'>>(query: Q, ...args): Promise<ReturnType<Q>>;
}Usage:
export const list = query({
args: {},
handler: async (ctx) => await ctx.db.query('messages').collect(),
});
export const getByAuthor = query({
args: { author: v.string() },
handler: async (ctx, args) =>
await ctx.db.query('messages')
.withIndex('by_author', q => q.eq('author', args.author))
.collect(),
});
// With auth
export const myPosts = query({
handler: async (ctx) => {
const identity = await ctx.auth.getUserIdentity();
if (!identity) return [];
return await ctx.db.query('posts')
.withIndex('by_author', q => q.eq('author', identity.subject))
.collect();
},
});function mutation<ArgsValidator, Output>(func: {
args?: ArgsValidator;
handler: (ctx: MutationCtx, args: ObjectType<ArgsValidator>) => Promise<Output>;
}): RegisteredMutation<'public', ArgsValidator, Output>;
interface MutationCtx {
db: GenericDatabaseWriter; // Read-write (insert/patch/replace/delete)
auth: Auth;
storage: StorageWriter;
scheduler: Scheduler;
runQuery<Q>(query: Q, ...args): Promise<ReturnType<Q>>;
runMutation<M>(mutation: M, ...args): Promise<ReturnType<M>>;
}Usage:
export const create = mutation({
args: { author: v.string(), body: v.string() },
handler: async (ctx, args) =>
await ctx.db.insert('messages', { author: args.author, body: args.body }),
});
export const update = mutation({
args: { id: v.id('messages'), body: v.string() },
handler: async (ctx, args) => {
await ctx.db.patch(args.id, { body: args.body });
},
});
export const like = mutation({
args: { id: v.id('messages') },
handler: async (ctx, args) => {
const msg = await ctx.db.get(args.id);
if (!msg) throw new Error('Not found');
await ctx.db.patch(args.id, { likes: msg.likes + 1 });
},
});
// Atomic multi-operation
export const transfer = mutation({
args: { fromId: v.id('accounts'), toId: v.id('accounts'), amount: v.number() },
handler: async (ctx, args) => {
const from = await ctx.db.get(args.fromId);
const to = await ctx.db.get(args.toId);
// Both succeed or both fail
await ctx.db.patch(args.fromId, { balance: from.balance - args.amount });
await ctx.db.patch(args.toId, { balance: to.balance + args.amount });
},
});function action<ArgsValidator, Output>(func: {
args?: ArgsValidator;
handler: (ctx: ActionCtx, args: ObjectType<ArgsValidator>) => Promise<Output>;
}): RegisteredAction<'public', ArgsValidator, Output>;
interface ActionCtx {
runQuery<Q>(query: Q, ...args): Promise<ReturnType<Q>>;
runMutation<M>(mutation: M, ...args): Promise<ReturnType<M>>;
runAction<A>(action: A, ...args): Promise<ReturnType<A>>;
scheduler: Scheduler;
auth: Auth;
storage: StorageActionWriter; // get/store files
vectorSearch(table: string, index: string): VectorSearch;
}Usage:
export const sendEmail = action({
args: { to: v.string(), subject: v.string(), body: v.string() },
handler: async (ctx, args) => {
await fetch('https://api.sendgrid.com/v3/mail/send', {
method: 'POST',
headers: { 'Authorization': `Bearer ${process.env.SENDGRID_KEY}` },
body: JSON.stringify({ to: args.to, subject: args.subject, html: args.body }),
});
await ctx.runMutation(api.logs.record, { event: 'email_sent', to: args.to });
},
});
export const generateSummary = action({
args: { text: v.string() },
handler: async (ctx, args) => {
const response = await fetch('https://api.openai.com/v1/chat/completions', {
method: 'POST',
headers: {
'Authorization': `Bearer ${process.env.OPENAI_KEY}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({
model: 'gpt-4',
messages: [{ role: 'user', content: `Summarize: ${args.text}` }],
}),
});
const data = await response.json();
return data.choices[0].message.content;
},
});
// Vector search
export const semanticSearch = action({
args: { query: v.string() },
handler: async (ctx, args) => {
// Get embedding from external API
const embeddingResponse = await fetch('https://api.openai.com/v1/embeddings', {
method: 'POST',
headers: { 'Authorization': `Bearer ${process.env.OPENAI_KEY}` },
body: JSON.stringify({ model: 'text-embedding-ada-002', input: args.query }),
});
const { data } = await embeddingResponse.json();
// Search in Convex
return await ctx.vectorSearch('documents', 'by_embedding', {
vector: data[0].embedding,
limit: 10,
});
},
});function httpAction(
handler: (ctx: ActionCtx, request: Request) => Promise<Response>
): PublicHttpAction;Usage:
// convex/http.ts
import { httpRouter, httpAction } from 'convex/server';
const http = httpRouter();
http.route({
path: '/webhook',
method: 'POST',
handler: httpAction(async (ctx, request) => {
const signature = request.headers.get('X-Signature');
if (!verifySignature(signature)) {
return new Response('Unauthorized', { status: 401 });
}
const body = await request.json();
await ctx.runMutation(api.webhooks.process, body);
return new Response(JSON.stringify({ success: true }), {
headers: { 'Content-Type': 'application/json' },
});
}),
});
http.route({
path: '/api/users',
method: 'GET',
handler: httpAction(async (ctx, request) => {
const url = new URL(request.url);
const id = url.searchParams.get('id');
const user = await ctx.runQuery(api.users.get, { id });
return new Response(JSON.stringify(user), {
headers: { 'Content-Type': 'application/json' },
});
}),
});
export default http;For server-only functions not exposed to clients:
// Internal query
export const _getSecret = internalQuery({
args: { id: v.id('secrets') },
handler: async (ctx, args) => await ctx.db.get(args.id),
});
// Internal mutation
export const _cleanup = internalMutation({
args: { olderThan: v.number() },
handler: async (ctx, args) => {
const old = await ctx.db.query('logs')
.filter(q => q.lt(q.field('_creationTime'), args.olderThan))
.collect();
for (const log of old) await ctx.db.delete(log._id);
},
});
// Internal action
export const _processWebhook = internalAction({
args: { data: v.any() },
handler: async (ctx, args) => {
await fetch('https://external-service.com/process', {
method: 'POST',
body: JSON.stringify(args.data),
});
},
});
// Call internal functions
import { internal } from './_generated/api';
export const scheduleCleanup = mutation({
handler: async (ctx) => {
await ctx.scheduler.runAfter(0, internal.logs._cleanup, {
olderThan: Date.now() - 7 * 24 * 60 * 60 * 1000,
});
},
});const identity = await ctx.auth.getUserIdentity();
if (!identity) throw new Error('Not authenticated');
// Check role
const role = identity.role as string | undefined;
if (role !== 'admin') throw new Error('Not authorized');import { ConvexError } from 'convex/values';
// String error
throw new ConvexError('User not found');
// Structured error
throw new ConvexError({
code: 'VALIDATION_ERROR',
field: 'email',
message: 'Invalid email format',
});// In queries: only runQuery
const data = await ctx.runQuery(api.module.otherQuery, { arg: 'value' });
// In mutations: runQuery + runMutation
const data = await ctx.runQuery(api.module.query, {});
await ctx.runMutation(api.module.otherMutation, {});
// In actions: runQuery + runMutation + runAction
const data = await ctx.runQuery(api.module.query, {});
await ctx.runMutation(api.module.mutation, {});
await ctx.runAction(api.module.otherAction, {});export const search = query({
args: {
query: v.string(),
limit: v.optional(v.number()),
category: v.optional(v.string()),
},
handler: async (ctx, args) => {
const limit = args.limit ?? 10;
let q = ctx.db.query('items');
if (args.category) {
q = q.withIndex('by_category', q => q.eq('category', args.category));
}
return await q.take(limit);
},
});