Skip to main content

Convex Overview

Convex is the open source, reactive database where queries are TypeScript code running right in the database. Just like React components react to state changes, Convex queries react to database changes.

Convex provides a database, a place to write your server functions, and client libraries. It makes it easy to build and scale dynamic live-updating apps.

The following diagram shows the standard three-tier app architecture that Convex enables. We'll start at the bottom and work our way up to the top of this diagram.

Convex in your app

Database

The database is at the core of Convex. The Convex database is automatically provisioned when you create your project. There is no connection setup or cluster management.

info

In Convex, your database queries are just TypeScript code written in your server functions. There is no SQL to write. There are no ORMs needed.

The Convex database is reactive. Whenever any data a query depends on changes, the query is rerun, and client subscriptions are updated.

Convex is a "document-relational" database. "Document" means you put JSON-like nested objects into your database. "Relational" means you have tables with relations, like tasks assigned to a user using IDs to reference documents in other tables.

The Convex cloud offering runs on top of Amazon RDS using MySQL as its persistence layer. The Open Source version uses SQLite, and soon Postgres or MySQL. The database is ACID-compliant and uses serializable isolation and optimistic concurrency control. All that to say, Convex provides the strictest possible transactional guarantees, and you never see inconsistent data.

Server functions

When you create a new Convex project, you automatically get a convex/ folder where you write your server functions. This is where all your backend application logic and database query code live.

Example TypeScript server functions that read (query) and write (mutation) to the database.

convex/tasks.ts
// A Convex query function
export const getAllOpenTasks = query({
args: {},
handler: async (ctx, args) => {
// Query the database to get all items that are not completed
const tasks = await ctx.db
.query("tasks")
.withIndex("by_completed", (q) => q.eq("completed", false))
.collect();
return tasks;
},
});

// A Convex mutation function
export const setTaskCompleted = mutation({
args: { taskId: v.id("tasks"), completed: v.boolean() },
handler: async (ctx, { taskId, completed }) => {
// Update the database using TypeScript
await ctx.db.patch(taskId, { completed });
},
});

You read and write to your database through query or mutation functions. Query functions are pure functions that can only read from the database. Mutation functions are transactions that can read or write from the database. These two database functions are not allowed to take any non-deterministic actions like network requests to ensure transactional guarantees.

info

The entire Convex mutation function is a transaction. There are no begin or end transaction statements to write. Convex automatically retries the function on conflicts, and you don't have to manage anything.

Convex also provides standard general-purpose serverless functions called actions. Action functions can make network requests. They have to call query or mutation functions to read and write to the database. You use actions to call LLMs or send emails.

You can also durably schedule Convex functions via the scheduler or cron jobs. Scheduling lets you build workflows like emailing a new user a day later if they haven't performed an onboarding task.

You call your Convex functions via client libraries or directly via HTTP.

Client libraries

Convex client libraries keep your frontend synced with the results of your server functions.

// In your React component
import { useQuery } from "convex/react";
import { api } from "../convex/_generated/api";

export function TaskList() {
const data = useQuery(api.tasks.getAllOpenTasks);
return data ?? "Loading...";
}

Like the useState hook that updates your React component when local state changes, the Convex useQuery hook automatically updates your component whenever the result of your query changes. There's no manual subscription management or state synchronization needed.

When calling query functions, the client library subscribes to the results of the function. Convex tracks the dependencies of your query functions, including what data was read from the database. Whenever relevant data in the database changes, the Convex automatically reruns the query and sends the result to the client.

The client library also queues up mutations in memory to send to the server. As mutations execute and cause query results to update, the client library keeps your app state consistent. It updates all subscriptions to the same logical moment in time in the database.

Convex provides client libraries for nearly all popular web and native app frameworks. Client libraries connect to your Convex deployment via WebSockets. You can then call your public Convex functions through the library. Convex also use Convex with HTTP directly, you just won't get the automatic subscriptions.

Putting it all together

Let's return to the getAllOpenTasks Convex query function from earlier that gets all tasks that are not marked as completed:

convex/tasks.ts
export const getAllOpenTasks = query({
args: {},
handler: async (ctx, args) => {
// Query the database to get all items that are not completed
const tasks = await ctx.db
.query("tasks")
.withIndex("by_completed", (q) => q.eq("completed", false))
.collect();
return tasks;
},
});

Let's follow along what happens when you subscribe to this query:

Convex data flow

The web app uses the useQuery hook to subscribe to this query, and the following happens to get an initial value:

  • The Convex client sends a message to the Convex server to subscribe to the query
  • The Convex server runs the function, which reads data from the database
  • The Convex server sends a message to the client with the function's result

In this case the initial result looks like this (1):

[
{ _id: "e4g", title: "Grocery shopping", complete: false },
{ _id: "u9v", title: "Plant new flowers", complete: false },
];

Then you use a mutation to mark an item as completed (2). Convex then reruns the query (3) to get an updated result. And pushes the result to the web app via the WebSocket connection (4):

[
{ _id: "e4g", title: "Grocery shopping", complete: false },
];

Beyond reactivity

Beyond reactivity, Convex's architecture is crucial for a deeper reason. Convex does not let your app have inconsistent state at any layer of the stack.

To illustrate this, let's imagine you're building a shopping cart for an e-commerce store.

Convex in your app

On the product listing page, you have two numbers, one showing the number of items remaining in stock and another showing the number of items in your shopping cart. Each number is a result of a different query function.

Every time you press the "Add to Cart" button, a mutation is called to remove one item from the stock and add it to the shopping cart.

The mutation to change the cart runs in a transaction, so your database is always in a consistent state. The reactive database knows that the queries showing the number of items in stock and the number of items in the shopping cart both need to be updated. The queries are invalidated and rerun. The results are pushed to the web app via the WebSocket connection.

The client library makes sure that both queries update at the same time in the web app since they reflect a singular moment in time in your database. You never have a moment where those numbers don't add up. Your app always shows consistent data.

You can see this example in action in the Swaghaus sample app.

For human and AI generated code

Convex is designed around a small set of composable abstractions with strong guarantees that result in code that is not only faster to write, it’s easier to read and maintain, whether written by a team member or an LLM. Key features make sure you get bug-free AI generated code:

  1. Queries are Just TypeScript Your database queries are pure TypeScript functions with end-to-end type safety and IDE support. This means AI can generate database code using the large training set of TypeScript code without switching to SQL.
  2. Less Code for the Same Work Since so much infrastructure and boiler plate is automatically manged by Convex there is less code to write, and thus less code to get wrong.
  3. Automatic Reactivity The reactive system automatically tracks data dependencies and updates your UI. AI doesn't need to manually manage subscriptions, WebSocket connections, or complex state synchronization—Convex handles all of this automatically.
  4. Transactional Guarantees Queries are read-only and mutations run in transactions. These constraints make it nearly impossible for AI to write code that could corrupt your data or leave your app in an inconsistent state.

Together, these features mean AI can focus on your business logic while Convex's guarantees prevent common failure modes.

Learn more

If you are intrigued about the details of how Convex pulls this all off, you can read Convex co-founder Sujay's excellent How Convex Works blog post.

Now that you have a good sense of how Convex fits in your app. Let's walk through the overall workflow of setting up and launching a Convex app.