convex-backend
Convex is an open-source reactive database and backend platform designed to make building live-updating applications easy for web developers. It provides a unified solution for database storage, serve
Convex
Convex is an open-source reactive database and backend platform designed to make building live-updating applications easy for web developers. It provides a unified solution for database storage, server-side functions, and client libraries with automatic real-time synchronization, strong consistency guarantees, and built-in file storage, search, scheduling, and vector search capabilities—all accessed by writing pure TypeScript.
Quick References
| File | Purpose |
|---|---|
npm-packages/convex/src/index.ts | Main package entry point |
npm-packages/convex/src/server/index.ts | Server SDK entry point |
npm-packages/convex/src/react/index.ts | React hooks and components |
npm-packages/convex/src/browser/index.ts | Browser/Node.js HTTP client |
README.md | Project documentation |
When to Use
- Building real-time collaborative apps: When you need data to sync automatically across clients like chat, multiplayer games, or live dashboards
- Simplifying backend logic: When you want to write your database queries and business logic in TypeScript without managing separate databases and API servers
- Reactive applications: When you need UI components to automatically update when data changes without manual refresh logic
- Self-contained backend: When you want database, functions, and authentication in one platform without managing complex infrastructure
- Quick prototyping to production: When you need a platform that scales from development to production with the same codebase
Installation
# Install the Convex SDK and CLI
npm install convex
Convex is typically used with code generation. After installing, run:
# Initialize a new Convex project
npx convex dev
# This will generate typed API code in convex/_generated/
# and set up your development environment
You'll need to set your Convex deployment URL, typically via environment variable:
# For Convex Cloud (free tier available)
CONVEX_URL=https://your-project-name.convex.cloud
Best Practices
-
Use code generation for type safety: Always use generated API types from
convex/_generated/apirather than untyped functions. The generated code provides compile-time type checking for all your queries, mutations, and schemas. -
Define schema upfront: Use
defineSchemaanddefineTableto model your data structure. Define indexes and search indexes on tables to optimize performance and enable complex queries. -
Separate public and internal functions: Mark functions with visibility (
publicvsinternal) to control which functions are callable from the client. Internal functions are useful for scheduled tasks, cron jobs, and helper functions. -
Use queries for reads, mutations for writes: Queries are for read-only operations that don't modify data. Mutations can write to the database and should be used for operations that change state.
-
Authenticate user context: Access
ctx.authin server functions to get the current user's identity and permissions, then use it to enforce access control on a per-document basis. -
Handle file storage properly: Use
ctx.storage.generateUploadUrl()to create upload URLs, then store the returnedId<"_storage">reference in your documents rather than storing raw data. -
Use scheduling for background jobs: Use
ctx.scheduler.runAfter()for delayed actions andcronJobs()for recurring tasks, but prefer functions marked withinternalMutationfor scheduled work to avoid exposing them publicly. -
Optimize query chains with indexes: Always define indexes for fields you query on (
.order(),.filter(),.eq()) and use.withIndex()to avoid full table scans in production. -
Validate all arguments: Use the
vvalidators fromconvex/valuesto enforce schema rules on function arguments. This provides runtime validation and TypeScript types. -
Use pagination for large datasets: For tables that will grow large, use
.paginate()with.cursor()based pagination rather than.collect()to avoid loading all records into memory.
Common Patterns
Defining a database schema:
import { defineSchema, defineTable } from "convex/server";
import { v } from "convex/values";
export default defineSchema({
// Define a table with fields and optional rules
users: defineTable({
name: v.string(),
email: v.optional(v.string()),
tokenIdentifier: v.string(),
})
.index("by_token", ["tokenIdentifier"])
.searchIndex("search_name", { searchFields: ["name"] }),
messages: defineTable({
body: v.string(),
author: v.id("users"),
})
.index("by_author", ["author"])
.searchIndex("search_body", { searchFields: ["body"] }),
});
Creating a query function:
import { query } from "./_generated/server";
export const list = query({
args: { limit: v.optional(v.number()) },
handler: async (ctx, args) => {
// Fetch all messages or use pagination
const messages = await ctx.db.query("messages")
.order("desc")
.take(args.limit ?? 100);
return messages;
},
});
Creating a mutation function:
import { mutation } from "./_generated/server";
export const send = mutation({
args: { body: v.string(), author: v.string() },
handler: async (ctx, { body, author }) => {
const message = { body, author };
// Insert returns the new document's ID
const messageId = await ctx.db.insert("messages", message);
return messageId;
},
});
Using React hooks to call functions:
import { useQuery, useMutation } from "convex/react";
import { api } from "../convex/_generated/api";
function ChatApp() {
// Query data - automatically subscribes to updates
const messages = useQuery(api.messages.list) || [];
// Get a mutation function
const sendMessage = useMutation(api.messages.send);
const handleSend = async () => {
await sendMessage({ body: "Hello!", author: "User" });
};
return (
<div>
<ul>
{messages.map(msg => (
<li key={msg._id}>
{msg.author}: {msg.body}
</li>
))}
</ul>
<button onClick={handleSend}>Send</button>
</div>
);
}
Setting up Convex in a React application:
import { ConvexProvider, ConvexReactClient } from "convex/react";
import ReactDOM from "react-dom/client";
// Create the Convex client
const convex = new ConvexReactClient(process.env.VITE_CONVEX_URL!);
ReactDOM.createRoot(document.getElementById("root")!).render(
<ConvexProvider client={convex}>
<App />
</ConvexProvider>
);
Authentication with user identity:
import { mutation } from "./_generated/server";
export const storeUser = mutation({
args: {},
handler: async ({ db, auth }) => {
const identity = await auth.getUserIdentity();
if (!identity) {
throw new Error("Unauthenticated");
}
// Check if user already exists
const existingUser = await db
.query("users")
.withIndex("by_token", q => q.eq("tokenIdentifier", identity.tokenIdentifier))
.unique();
if (existingUser) {
return existingUser._id;
}
// Create new user
return db.insert("users", {
name: identity.name!,
tokenIdentifier: identity.tokenIdentifier,
});
},
});
File storage pattern:
import { mutation, query } from "./_generated/server";
import { v } from "convex/values";
export const generateUploadUrl = mutation({
args: {},
handler: async (ctx) => {
// Generate a temporary upload URL
return await ctx.storage.generateUploadUrl();
},
});
export const sendImage = mutation({
args: { storageId: v.id("_storage"), author: v.string() },
handler: async (ctx, { storageId, author }) => {
// Store the storage ID reference in your document
await ctx.db.insert("messages", {
body: storageId,
author,
format: "image",
});
},
});
export const listMessages = query({
args: {},
handler: async (ctx) => {
const messages = await ctx.db.query("messages").collect();
// Expand storage IDs to URLs for client
return await Promise.all(
messages.map(async (msg) => ({
...msg,
url: await ctx.storage.getUrl(msg.body),
})),
);
},
});
Search with text search indexes:
import { query } from "./_generated/server";
export const searchMessages = query({
args: { query: v.string() },
handler: async (ctx, { query }) => {
return await ctx.db
.query("messages")
.withSearchIndex("search_body", (q) => q.search("body", query))
.take(10);
},
});
Scheduling background jobs:
import { mutation, internalMutation } from