TypeScript backend SDK, client library, and CLI for building real-time applications on the Convex platform
npx @tessl/cli install tessl/npm-convex@1.29.0TypeScript-first backend platform: real-time database + serverless functions + reactive queries.
Package: npm install convex (v1.29.3)
// Schema (convex/schema.ts)
import { defineSchema, defineTable } from 'convex/server';
import { v } from 'convex/values';
export default defineSchema({
messages: defineTable({
author: v.string(),
body: v.string(),
}).index('by_author', ['author']),
});
// Backend (convex/messages.ts)
import { query, mutation } from './_generated/server';
import { v } from 'convex/values';
export const list = query({
args: {},
handler: async (ctx) => await ctx.db.query('messages').collect(),
});
export const send = mutation({
args: { author: v.string(), body: v.string() },
handler: async (ctx, args) => await ctx.db.insert('messages', args),
});
// React (src/App.tsx)
import { ConvexProvider, ConvexReactClient, useQuery, useMutation } from 'convex/react';
import { api } from '../convex/_generated/api';
const convex = new ConvexReactClient(process.env.REACT_APP_CONVEX_URL!);
function Messages() {
const messages = useQuery(api.messages.list);
const send = useMutation(api.messages.send);
return messages?.map(m => <div key={m._id}>{m.body}</div>);
}
function App() {
return <ConvexProvider client={convex}><Messages /></ConvexProvider>;
}// Server functions
import { query, mutation, action, internalQuery, internalMutation, internalAction, httpAction } from 'convex/server';
// Schema & validation
import { defineSchema, defineTable } from 'convex/server';
import { v } from 'convex/values';
// React
import { ConvexProvider, ConvexReactClient, useQuery, useMutation, useAction, usePaginatedQuery } from 'convex/react';
// Browser (non-React)
import { ConvexClient, ConvexHttpClient } from 'convex/browser';
// Next.js
import { preloadQuery, fetchQuery, fetchMutation, fetchAction } from 'convex/nextjs';
// Auth providers
import { ConvexProviderWithAuth0 } from 'convex/react-auth0';
import { ConvexProviderWithClerk } from 'convex/react-clerk';export const get = query({
args: { id: v.id('table') },
handler: async (ctx, args) => {
// ctx.db: read-only, ctx.auth, ctx.storage.getUrl()
return await ctx.db.get(args.id);
},
});export const create = mutation({
args: { text: v.string() },
handler: async (ctx, args) => {
// ctx.db: read-write (insert/patch/replace/delete)
// ctx.scheduler: schedule functions, ctx.storage: generateUploadUrl()
return await ctx.db.insert('table', { text: args.text });
},
});export const sendEmail = action({
args: { to: v.string(), body: v.string() },
handler: async (ctx, args) => {
// ctx.runQuery/runMutation/runAction, ctx.vectorSearch, ctx.storage.get/store()
await fetch('https://api.email.com/send', { method: 'POST', body: JSON.stringify(args) });
await ctx.runMutation(api.logs.record, { event: 'email_sent' });
},
});Server Functions | Database Operations | Schema
// Primitives
v.string(), v.number(), v.bigint(), v.boolean(), v.null(), v.bytes()
// Structures
v.array(v.string())
v.object({ name: v.string(), age: v.number() })
v.record(v.string(), v.any())
// Special
v.id('tableName') // Document ID
v.optional(v.string()) // Optional field
v.union(v.literal('a'), v.literal('b')) // Union type
v.nullable(v.string()) // string | null
// Usage
const userValidator = v.object({
name: v.string(),
age: v.number(),
email: v.optional(v.string()),
});
type User = Infer<typeof userValidator>; // { name: string; age: number; email?: string }// Fetch by ID
const doc = await ctx.db.get(id);
// Query
await ctx.db.query('table').collect(); // All
await ctx.db.query('table').order('desc').take(10); // First 10
await ctx.db.query('table').filter(q => q.gt(q.field('age'), 18)).collect();
// Index query
await ctx.db.query('table')
.withIndex('by_author', q => q.eq('author', 'Alice'))
.collect();
// Range query
await ctx.db.query('table')
.withIndex('by_time', q => q.gte('_creationTime', startTime).lt('_creationTime', endTime))
.collect();
// Write (mutations only)
const id = await ctx.db.insert('table', { field: 'value' });
await ctx.db.patch(id, { field: 'newValue' });
await ctx.db.replace(id, { field: 'completelyNew' });
await ctx.db.delete(id);// Query (reactive)
const data = useQuery(api.module.queryFn, { arg: 'value' });
if (data === undefined) return <div>Loading...</div>;
// Skip conditionally
const data = useQuery(api.module.queryFn, shouldLoad ? { arg: 'value' } : 'skip');
// Mutation
const mutateFn = useMutation(api.module.mutationFn);
await mutateFn({ arg: 'value' });
// Optimistic update
const like = useMutation(api.posts.like).withOptimisticUpdate((store, args) => {
const posts = store.getQuery(api.posts.list);
if (posts) {
store.setQuery(api.posts.list, {}, posts.map(p =>
p._id === args.postId ? { ...p, likes: p.likes + 1 } : p
));
}
});
// Action
const actionFn = useAction(api.module.actionFn);
await actionFn({ arg: 'value' });
// Pagination
const { results, status, loadMore } = usePaginatedQuery(
api.module.list,
{ filter: 'value' },
{ initialNumItems: 20 }
);// Server Component
import { preloadQuery } from 'convex/nextjs';
import { api } from '@/convex/_generated/api';
export default async function Page() {
const preloaded = await preloadQuery(api.messages.list);
return <ClientComponent preloaded={preloaded} />;
}
// Client Component
'use client';
import { usePreloadedQuery } from 'convex/react';
function ClientComponent({ preloaded }) {
const messages = usePreloadedQuery(preloaded); // Reactive
return <div>{messages.map(m => <p key={m._id}>{m.body}</p>)}</div>;
}
// Server Action
import { fetchMutation } from 'convex/nextjs';
export async function createPost(formData: FormData) {
'use server';
const title = formData.get('title') as string;
await fetchMutation(api.posts.create, { title });
revalidatePath('/posts');
}
// With auth (Clerk)
import { auth } from '@clerk/nextjs';
const { getToken } = auth();
const token = await getToken({ template: 'convex' });
const data = await fetchQuery(api.users.getCurrent, {}, { token });// Server-side
export const getCurrentUser = query({
handler: async (ctx) => {
const identity = await ctx.auth.getUserIdentity();
if (!identity) return null;
return {
id: identity.tokenIdentifier,
name: identity.name,
email: identity.email,
};
},
});
// React with Auth0
import { ConvexProviderWithAuth0 } from 'convex/react-auth0';
import { Auth0Provider } from '@auth0/auth0-react';
<Auth0Provider domain="..." clientId="...">
<ConvexProviderWithAuth0 client={convex}>
<App />
</ConvexProviderWithAuth0>
</Auth0Provider>
// React with Clerk
import { ConvexProviderWithClerk } from 'convex/react-clerk';
import { ClerkProvider, useAuth } from '@clerk/clerk-react';
<ClerkProvider publishableKey="...">
<ConvexProviderWithClerk client={convex} useAuth={useAuth}>
<App />
</ConvexProviderWithClerk>
</ClerkProvider>
// Conditional rendering
import { Authenticated, Unauthenticated } from 'convex/react';
<Authenticated><Dashboard /></Authenticated>
<Unauthenticated><Login /></Unauthenticated>// Backend
export const list = query({
args: { paginationOpts: paginationOptsValidator },
handler: async (ctx, args) =>
await ctx.db.query('table').order('desc').paginate(args.paginationOpts),
});
// React
const { results, status, loadMore } = usePaginatedQuery(api.module.list, {}, { initialNumItems: 20 });// Upload
export const generateUploadUrl = mutation(async (ctx) => await ctx.storage.generateUploadUrl());
// Client
const url = await convex.mutation(api.files.generateUploadUrl);
const result = await fetch(url, { method: 'POST', body: file });
const { storageId } = await result.json();
// Download
export const getUrl = query({
args: { storageId: v.string() },
handler: async (ctx, args) => await ctx.storage.getUrl(args.storageId),
});// Schedule function
export const schedule = mutation({
handler: async (ctx) => {
await ctx.scheduler.runAfter(60000, internal.task.run, { data: 'value' });
await ctx.scheduler.runAt(timestamp, internal.task.run, { data: 'value' });
},
});
// Cron (convex/crons.ts)
import { cronJobs } from 'convex/server';
const crons = cronJobs();
crons.daily('cleanup', { hourUTC: 2, minuteUTC: 0 }, internal.tasks.cleanup);
export default crons;// convex/http.ts
import { httpRouter, httpAction } from 'convex/server';
const http = httpRouter();
http.route({
path: '/webhook',
method: 'POST',
handler: httpAction(async (ctx, request) => {
const body = await request.json();
await ctx.runMutation(api.webhooks.process, body);
return new Response(JSON.stringify({ success: true }), {
headers: { 'Content-Type': 'application/json' },
});
}),
});
export default http;export const search = action({
args: { embedding: v.array(v.float64()) },
handler: async (ctx, args) => {
const results = await ctx.vectorSearch('documents', 'by_embedding', {
vector: args.embedding,
limit: 10,
filter: q => q.eq('category', 'news'),
});
return results;
},
});// Document types from schema
type Message = Doc<'messages'>; // Includes _id, _creationTime
type MessageInput = WithoutSystemFields<Message>; // For inserts
// Function references
api.module.functionName // Type-safe reference
internal.module.functionName // Internal functions
// Extract types from validators
type User = Infer<typeof userValidator>;
// Field paths
type MessageFieldPaths = FieldPaths<'messages'>; // 'author' | 'body' | '_id' | '_creationTime'const identity = await ctx.auth.getUserIdentity();
if (!identity) throw new Error('Not authenticated');import { ConvexError } from 'convex/values';
throw new ConvexError({ code: 'NOT_FOUND', id: args.id });const user = await ctx.db.query('users')
.withIndex('by_email', q => q.eq('email', args.email))
.unique();export const transfer = mutation({
handler: async (ctx, args) => {
const from = await ctx.db.get(args.fromId);
const to = await ctx.db.get(args.toId);
// All succeed or all fail
await ctx.db.patch(args.fromId, { balance: from.balance - args.amount });
await ctx.db.patch(args.toId, { balance: to.balance + args.amount });
},
});