Convex
Convex real-time backend with reactive queries, mutations, and serverless functions
You are an expert in Convex for rapid backend development, including its reactive database, server functions, file storage, scheduling, and real-time sync. ## Key Points - Define a schema in `convex/schema.ts` — it provides type safety across your entire stack and enables Convex to validate data at write time. - Use indexes for any query that filters or sorts; Convex does not scan tables without indexes. - Keep queries and mutations deterministic (no `Date.now()`, no `Math.random()`) — use `action` for side effects. - Use `ctx.scheduler.runAfter` to defer slow work (email, webhooks) from mutations to actions. - Leverage the automatic real-time sync: `useQuery` re-renders when data changes with zero extra code. - **Side effects in queries/mutations**: Queries and mutations run in a deterministic sandbox — external API calls, `Date.now()`, and random number generation must go in `action` functions. - **Not using indexes**: Queries without an appropriate index will throw an error at runtime. - **Large transactions**: Mutations that read or write too many documents in a single transaction can hit Convex limits; break large operations into batches using `ctx.scheduler`. - **Forgetting argument validation**: Always define `args` with `v` validators; this ensures type safety and prevents malicious inputs. - **Ignoring pagination**: For large result sets, use `.paginate(opts)` instead of `.collect()` to avoid loading everything into memory. ## Quick Example ```bash # Start the Convex dev server (watches for changes, pushes functions) npx convex dev # Deploy to production npx convex deploy ```
skilldb get baas-skills/ConvexFull skill: 292 linesConvex — Backend as a Service
You are an expert in Convex for rapid backend development, including its reactive database, server functions, file storage, scheduling, and real-time sync.
Core Philosophy
Overview
Convex is a full-stack reactive backend platform. It provides a document database with automatic real-time sync to clients, server-side functions (queries, mutations, actions) written in TypeScript/JavaScript, file storage, cron jobs, and built-in search. All server functions run in a transactional, deterministic runtime. Convex eliminates the need for manual caching, WebSocket plumbing, or separate API layers.
Setup & Configuration
Project Initialization
# Create a new project
npm create convex@latest
# Or add to an existing project
npm install convex
npx convex init
Convex Provider (React)
// src/main.tsx
import { ConvexProvider, ConvexReactClient } from 'convex/react';
const convex = new ConvexReactClient(import.meta.env.VITE_CONVEX_URL);
function App() {
return (
<ConvexProvider client={convex}>
<MainApp />
</ConvexProvider>
);
}
Development Workflow
# Start the Convex dev server (watches for changes, pushes functions)
npx convex dev
# Deploy to production
npx convex deploy
Core Patterns
Schema Definition
// convex/schema.ts
import { defineSchema, defineTable } from 'convex/server';
import { v } from 'convex/values';
export default defineSchema({
posts: defineTable({
title: v.string(),
body: v.string(),
authorId: v.id('users'),
published: v.boolean(),
})
.index('by_author', ['authorId'])
.index('by_published', ['published']),
users: defineTable({
name: v.string(),
email: v.string(),
tokenIdentifier: v.string(),
}).index('by_token', ['tokenIdentifier']),
});
Queries (Reactive Reads)
// convex/posts.ts
import { query } from './_generated/server';
import { v } from 'convex/values';
export const list = query({
args: { published: v.optional(v.boolean()) },
handler: async (ctx, args) => {
if (args.published !== undefined) {
return await ctx.db
.query('posts')
.withIndex('by_published', (q) => q.eq('published', args.published!))
.order('desc')
.take(20);
}
return await ctx.db.query('posts').order('desc').take(20);
},
});
export const get = query({
args: { id: v.id('posts') },
handler: async (ctx, args) => {
return await ctx.db.get(args.id);
},
});
Mutations (Writes)
// convex/posts.ts
import { mutation } from './_generated/server';
import { v } from 'convex/values';
export const create = mutation({
args: {
title: v.string(),
body: v.string(),
},
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');
return await ctx.db.insert('posts', {
title: args.title,
body: args.body,
authorId: user._id,
published: false,
});
},
});
export const remove = mutation({
args: { id: v.id('posts') },
handler: async (ctx, args) => {
await ctx.db.delete(args.id);
},
});
Actions (Side Effects)
// convex/actions.ts
import { action } from './_generated/server';
import { v } from 'convex/values';
import { api } from './_generated/api';
export const sendEmail = action({
args: { to: v.string(), subject: v.string(), body: v.string() },
handler: async (ctx, args) => {
// Actions can call external APIs
const response = await fetch('https://api.resend.com/emails', {
method: 'POST',
headers: {
Authorization: `Bearer ${process.env.RESEND_API_KEY}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({
from: 'noreply@example.com',
to: args.to,
subject: args.subject,
html: args.body,
}),
});
// Actions can call mutations to write back to the database
await ctx.runMutation(api.logs.create, {
type: 'email_sent',
details: `Email sent to ${args.to}`,
});
return response.ok;
},
});
Using from React
import { useQuery, useMutation } from 'convex/react';
import { api } from '../convex/_generated/api';
function PostList() {
// Automatically re-renders when data changes
const posts = useQuery(api.posts.list, { published: true });
const createPost = useMutation(api.posts.create);
if (posts === undefined) return <div>Loading...</div>;
return (
<div>
<button onClick={() => createPost({ title: 'New', body: 'Content' })}>
Add Post
</button>
{posts.map((post) => (
<div key={post._id}>{post.title}</div>
))}
</div>
);
}
File Storage
// Generate upload URL (mutation)
export const generateUploadUrl = mutation(async (ctx) => {
return await ctx.storage.generateUploadUrl();
});
// Store the storage ID after upload (mutation)
export const saveFile = mutation({
args: { storageId: v.id('_storage'), postId: v.id('posts') },
handler: async (ctx, args) => {
await ctx.db.patch(args.postId, { imageId: args.storageId });
},
});
// Get the URL for a stored file (query)
export const getImageUrl = query({
args: { storageId: v.id('_storage') },
handler: async (ctx, args) => {
return await ctx.storage.getUrl(args.storageId);
},
});
Authentication (with Clerk)
// convex/auth.config.ts
export default {
providers: [
{
domain: process.env.CLERK_JWT_ISSUER_DOMAIN,
applicationID: 'convex',
},
],
};
// Client-side integration
import { ConvexProviderWithClerk } from 'convex/react-clerk';
import { ClerkProvider, useAuth } from '@clerk/clerk-react';
<ClerkProvider publishableKey={CLERK_KEY}>
<ConvexProviderWithClerk client={convex} useAuth={useAuth}>
<App />
</ConvexProviderWithClerk>
</ClerkProvider>
Best Practices
- Define a schema in
convex/schema.ts— it provides type safety across your entire stack and enables Convex to validate data at write time. - Use indexes for any query that filters or sorts; Convex does not scan tables without indexes.
- Keep queries and mutations deterministic (no
Date.now(), noMath.random()) — useactionfor side effects. - Use
ctx.scheduler.runAfterto defer slow work (email, webhooks) from mutations to actions. - Leverage the automatic real-time sync:
useQueryre-renders when data changes with zero extra code.
Common Pitfalls
- Side effects in queries/mutations: Queries and mutations run in a deterministic sandbox — external API calls,
Date.now(), and random number generation must go inactionfunctions. - Not using indexes: Queries without an appropriate index will throw an error at runtime.
- Large transactions: Mutations that read or write too many documents in a single transaction can hit Convex limits; break large operations into batches using
ctx.scheduler. - Forgetting argument validation: Always define
argswithvvalidators; this ensures type safety and prevents malicious inputs. - Ignoring pagination: For large result sets, use
.paginate(opts)instead of.collect()to avoid loading everything into memory.
Anti-Patterns
Over-engineering for hypothetical requirements. Building for scenarios that may never materialize adds complexity without value. Solve the problem in front of you first.
Ignoring the existing ecosystem. Reinventing functionality that mature libraries already provide wastes time and introduces risk.
Premature abstraction. Creating elaborate frameworks before having enough concrete cases to know what the abstraction should look like produces the wrong abstraction.
Neglecting error handling at system boundaries. Internal code can trust its inputs, but boundaries with external systems require defensive validation.
Skipping documentation. What is obvious to you today will not be obvious to your colleague next month or to you next year.
Install this skill directly: skilldb add baas-skills
Related Skills
Appwrite
Appwrite self-hosted BaaS with database, auth, storage, and serverless functions
AWS Amplify
AWS Amplify BaaS with AppSync GraphQL, Cognito auth, S3 storage, and Lambda functions
Backendless
Backendless BaaS with real-time database, user authentication, Cloud Code,
Clerk Auth
Clerk authentication service with pre-built UI components, session management, and multi-framework support
Encore
Encore is a backend development platform that automatically provisions, configures, and manages cloud infrastructure based on your Go code. It simplifies building and deploying cloud-native applications by allowing you to focus purely on business logic.
Firebase
Firebase BaaS with Firestore, Authentication, Cloud Functions, and Hosting