import { query } from './_generated/server';
import { paginationOptsValidator } from 'convex/server';
export const listMessages = query({
args: { paginationOpts: paginationOptsValidator },
handler: async (ctx, args) =>
await ctx.db.query('messages')
.order('desc')
.paginate(args.paginationOpts),
});
// Returns: { page: Item[], isDone: boolean, continueCursor: string }
// With filters
export const listByAuthor = query({
args: {
author: v.string(),
paginationOpts: paginationOptsValidator,
},
handler: async (ctx, args) =>
await ctx.db.query('messages')
.withIndex('by_author', q => q.eq('author', args.author))
.paginate(args.paginationOpts),
});import { usePaginatedQuery } from 'convex/react';
function MessageList() {
const { results, status, loadMore } = usePaginatedQuery(
api.messages.listMessages,
{},
{ initialNumItems: 20 }
);
return (
<div>
{results.map(msg => <div key={msg._id}>{msg.body}</div>)}
{status === 'CanLoadMore' && (
<button onClick={() => loadMore(20)}>Load More</button>
)}
{status === 'LoadingMore' && <div>Loading...</div>}
{status === 'Exhausted' && <div>No more items</div>}
</div>
);
}// Backend: generate URL
export const generateUploadUrl = mutation(async (ctx) =>
await ctx.storage.generateUploadUrl()
);
// Client: upload file
async function uploadFile(file: File) {
// 1. Get upload URL
const uploadUrl = await convex.mutation(api.files.generateUploadUrl);
// 2. Upload to Convex
const result = await fetch(uploadUrl, {
method: 'POST',
headers: { 'Content-Type': file.type },
body: file,
});
const { storageId } = await result.json();
// 3. Save reference in database
await convex.mutation(api.files.save, {
storageId,
name: file.name,
type: file.type,
size: file.size,
});
}
// Backend: save reference
export const save = mutation({
args: {
storageId: v.string(),
name: v.string(),
type: v.string(),
size: v.number(),
},
handler: async (ctx, args) =>
await ctx.db.insert('files', args),
});// Backend: get URL (valid 1 hour)
export const getUrl = query({
args: { storageId: v.string() },
handler: async (ctx, args) =>
await ctx.storage.getUrl(args.storageId),
});
// Client
const url = useQuery(api.files.getUrl, { storageId });
if (url) {
<img src={url} alt="Image" />
}export const deleteFile = mutation({
args: { fileId: v.id('files') },
handler: async (ctx, args) => {
const file = await ctx.db.get(args.fileId);
if (!file) throw new Error('Not found');
await ctx.storage.delete(file.storageId);
await ctx.db.delete(args.fileId);
},
});// Download in action
export const processFile = action({
args: { storageId: v.string() },
handler: async (ctx, args) => {
const blob = await ctx.storage.get(args.storageId);
if (!blob) throw new Error('Not found');
const buffer = await blob.arrayBuffer();
// Process file...
},
});
// Upload in action
export const fetchAndStore = action({
args: { url: v.string() },
handler: async (ctx, args) => {
const response = await fetch(args.url);
const blob = await response.blob();
const storageId = await ctx.storage.store(blob);
await ctx.runMutation(api.files.save, {
storageId,
name: 'downloaded',
type: blob.type,
});
},
});// Schedule for later
export const scheduleReminder = mutation({
args: { userId: v.id('users'), message: v.string(), delayMinutes: v.number() },
handler: async (ctx, args) => {
const delayMs = args.delayMinutes * 60 * 1000;
const jobId = await ctx.scheduler.runAfter(
delayMs,
internal.notifications.send,
{ userId: args.userId, message: args.message }
);
return jobId; // Id<'_scheduled_functions'>
},
});
// Schedule at specific time
export const schedulePublish = mutation({
args: { postId: v.id('posts'), publishAt: v.number() },
handler: async (ctx, args) => {
const jobId = await ctx.scheduler.runAt(
args.publishAt,
internal.posts.publish,
{ postId: args.postId }
);
await ctx.db.patch(args.postId, {
scheduledJobId: jobId,
status: 'scheduled',
});
},
});
// Cancel scheduled job
export const cancelJob = mutation({
args: { jobId: v.id('_scheduled_functions') },
handler: async (ctx, args) => {
await ctx.scheduler.cancel(args.jobId);
},
});
// Internal function (scheduled target)
export const send = internalMutation({
args: { userId: v.id('users'), message: v.string() },
handler: async (ctx, args) => {
await ctx.db.insert('notifications', {
userId: args.userId,
message: args.message,
read: false,
});
},
});// convex/crons.ts
import { cronJobs } from 'convex/server';
import { internal } from './_generated/api';
const crons = cronJobs();
// Every 5 minutes
crons.interval('cleanup', { minutes: 5 }, internal.tasks.cleanup);
// Hourly at minute 15
crons.hourly('send-digest', { minuteUTC: 15 }, internal.emails.sendDigest);
// Daily at 2 AM UTC
crons.daily('backup', { hourUTC: 2, minuteUTC: 0 }, internal.tasks.backup);
// Weekly on Monday at 9 AM UTC
crons.weekly('report', {
dayOfWeek: 'monday',
hourUTC: 9,
minuteUTC: 0,
}, internal.reports.generate);
// Monthly on 1st at midnight UTC
crons.monthly('billing', {
day: 1,
hourUTC: 0,
minuteUTC: 0,
}, internal.billing.process);
// Custom cron expression
crons.cron('health-check', '*/15 * * * *', internal.health.check);
// With arguments
crons.daily('summary', { hourUTC: 8, minuteUTC: 0 }, internal.emails.sendSummary, {
type: 'daily',
});
export default crons;// convex/http.ts
import { httpRouter, httpAction } from 'convex/server';
import { api } from './_generated/api';
const http = httpRouter();
// POST webhook
http.route({
path: '/webhook',
method: 'POST',
handler: httpAction(async (ctx, request) => {
const signature = request.headers.get('X-Signature');
if (!verifySignature(signature, await request.text())) {
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' },
});
}),
});
// GET endpoint
http.route({
path: '/api/users',
method: 'GET',
handler: httpAction(async (ctx, request) => {
const url = new URL(request.url);
const id = url.searchParams.get('id');
if (!id) {
return new Response('Missing id', { status: 400 });
}
const user = await ctx.runQuery(api.users.get, { id });
return new Response(JSON.stringify(user), {
headers: { 'Content-Type': 'application/json' },
});
}),
});
// File upload
http.route({
path: '/upload',
method: 'POST',
handler: httpAction(async (ctx, request) => {
const blob = await request.blob();
const storageId = await ctx.storage.store(blob);
await ctx.runMutation(api.files.save, {
storageId,
contentType: blob.type,
size: blob.size,
});
return new Response(JSON.stringify({ storageId }), {
status: 201,
headers: { 'Content-Type': 'application/json' },
});
}),
});
// Path prefix
http.route({
pathPrefix: '/api/admin/',
method: 'GET',
handler: httpAction(async (ctx, request) => {
const authHeader = request.headers.get('Authorization');
if (!authHeader) {
return new Response('Unauthorized', { status: 401 });
}
const url = new URL(request.url);
// Handle /api/admin/* routes
return new Response(`Admin route: ${url.pathname}`);
}),
});
export default http;defineTable({
content: v.string(),
embedding: v.array(v.float64()),
category: v.string(),
}).vectorIndex('by_embedding', {
vectorField: 'embedding',
dimensions: 1536, // OpenAI ada-002
filterFields: ['category'],
})export const semanticSearch = action({
args: { query: v.string(), category: v.optional(v.string()) },
handler: async (ctx, args) => {
// 1. Get embedding from external API
const embeddingRes = await fetch('https://api.openai.com/v1/embeddings', {
method: 'POST',
headers: {
'Authorization': `Bearer ${process.env.OPENAI_KEY}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({
model: 'text-embedding-ada-002',
input: args.query,
}),
});
const { data } = await embeddingRes.json();
const embedding = data[0].embedding;
// 2. Search in Convex
const results = await ctx.vectorSearch('documents', 'by_embedding', {
vector: embedding,
limit: 10,
filter: args.category
? q => q.eq('category', args.category)
: undefined,
});
// 3. Fetch full documents
const docs = await Promise.all(
results.map(r => ctx.runQuery(api.documents.get, { id: r._id }))
);
return docs.map((doc, i) => ({
...doc,
score: results[i]._score,
}));
},
});Reusable modules with namespaced resources:
// Define component
import { defineComponent } from 'convex/server';
export default defineComponent('myComponent');
// Use in app
import { defineApp } from 'convex/server';
import myComponent from './myComponent/convex.config';
export default defineApp({
myComponent: myComponent.use({ /* config */ }),
});
// Access from code
import { components } from './_generated/api';
await ctx.runQuery(components.myComponent.queries.getData, {});// anyApi: for projects without codegen
import { anyApi } from 'convex/server';
const result = await anyApi.module.function();
// Function name utilities
import { getFunctionName, makeFunctionReference } from 'convex/server';
const name = getFunctionName(api.messages.list); // "messages:list"
const ref = makeFunctionReference<'query'>('messages:list');
// Filter API
import { filterApi } from 'convex/server';
const queryOnlyApi = filterApi(
api,
(func): func is FunctionReference<'query'> => func._type === 'query'
);// Schema
defineTable({
title: v.string(),
body: v.string(),
author: v.string(),
category: v.string(),
}).searchIndex('search_body', {
searchField: 'body',
filterFields: ['author', 'category'],
})
// Query
export const search = query({
args: {
searchTerm: v.string(),
author: v.optional(v.string()),
},
handler: async (ctx, args) => {
let q = ctx.db.query('posts')
.withSearchIndex('search_body', q => q.search('body', args.searchTerm));
if (args.author) {
q = q.withSearchIndex('search_body', q =>
q.search('body', args.searchTerm).eq('author', args.author)
);
}
return await q.collect();
},
});import { BaseConvexClient } from 'convex/browser';
const client = new BaseConvexClient(url, onTransition);
const watch = client.watch(api.query, {}, { journal: true });
watch.onUpdate(() => {
const journal = watch.journal();
console.log('Execution time:', journal?.executionTime, 'ms');
console.log('Cached:', journal?.cached);
console.log('Log:', journal?.lines);
});