convex-backend

IndexedCommit: 98515ea0 pullsUpdated Jan 29, 2026

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

Install this reference
Reference

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

FilePurpose
npm-packages/convex/src/index.tsMain package entry point
npm-packages/convex/src/server/index.tsServer SDK entry point
npm-packages/convex/src/react/index.tsReact hooks and components
npm-packages/convex/src/browser/index.tsBrowser/Node.js HTTP client
README.mdProject 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

  1. Use code generation for type safety: Always use generated API types from convex/_generated/api rather than untyped functions. The generated code provides compile-time type checking for all your queries, mutations, and schemas.

  2. Define schema upfront: Use defineSchema and defineTable to model your data structure. Define indexes and search indexes on tables to optimize performance and enable complex queries.

  3. Separate public and internal functions: Mark functions with visibility (public vs internal) to control which functions are callable from the client. Internal functions are useful for scheduled tasks, cron jobs, and helper functions.

  4. 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.

  5. Authenticate user context: Access ctx.auth in server functions to get the current user's identity and permissions, then use it to enforce access control on a per-document basis.

  6. Handle file storage properly: Use ctx.storage.generateUploadUrl() to create upload URLs, then store the returned Id<"_storage"> reference in your documents rather than storing raw data.

  7. Use scheduling for background jobs: Use ctx.scheduler.runAfter() for delayed actions and cronJobs() for recurring tasks, but prefer functions marked with internalMutation for scheduled work to avoid exposing them publicly.

  8. 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.

  9. Validate all arguments: Use the v validators from convex/values to enforce schema rules on function arguments. This provides runtime validation and TypeScript types.

  10. 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

“Explore distant worlds.”

© 2026 Oscar Gabriel