Skip to main content

1: The reactor

You are here

Convex Tour

  1. The reactor

    Learn about Convex's reactive database accessed through TypeScript cloud functions

  2. Convex and your app
  3. The platform in action

The Convex Reactor diagram

The core of the Convex platform, the engine that powers everything else, is an innovative database that uses TypeScript cloud functions as its interface.

Together, the database and its functions form the reactor which gives you realtime updates, perfect caching, type safety, and ACID transactions - all out of the box.

In this section, you'll explore the reactor through the Convex Dashboard to get familiar with:

  • Tables that store your data as relational Documents
  • Query functions that read from your tables reactively
  • Mutation functions that write to your tables

Screenshot of a chat app

The Convex Dashboard is your hub for viewing and managing your Convex projects. From any Convex app, you can always quickly jump into the dashboard for that particular backend with the convex command. Let's do that now for our new chat app:

npx convex dashboard

Table timeโ€‹

In Convex, your data is stored as documents organized into tables. On the dashboard's Data tab you can view and edit tables and documents manually.

The 'messages' table in the dashboard's 'Data' pane

As you can see, our chat project has only one table, named messages. It contains documents with author and body string fields in addition to Convex-generated _id and _creationTime fields. The _id field is the document's primary key, and on the next page we'll explore how it can be used to create relationships between documents.

๐Ÿค” Is this a relational or NoSQL database?

Both! Why choose, when you can have the best of both worlds?

Convex lets you store documents with all the flexibility you'd expect from a NoSQL database while also supporting first-class relationships and multi-document ACID transactions. All your existing relational modeling techniques work great on Convex.

Read more: Document IDs: References and Relationships

Convex functionsโ€‹

We've seen the tables, but how does data get in and out of them? In Convex, you write TypeScript cloud functions to create, read, update, and delete your data. There is no extra query language like SQL or GraphQL โ€“ everything in Convex is 100% TypeScript.

๐Ÿค” What about JavaScript?

For sure, it is possible to write your Convex functions in good old JavaScript. But we recommend using Convex with TypeScript for the convenience and safety of end-to-end type safety.

In the future, we may support other languages than TS/JS, so stay tuned!

The Functions tab in the dashboard lets you view, run, and monitor your functions โ€“ click it now. In our chat app, you'll see we have a messages module with two functions: list and send.

When you drill down into a specific function, the dashboard displays the function's currently deployed source code. These functions live in your project's codebase under the convex/ folder. You develop them on your computer right beside the rest of your app, and Convex (specifically, the convex command) automatically keeps them in sync in the background by redeploying them when they change.

What about the init module I see there?

That module has a couple of functions that create some seed data for the chat, as a convenience for getting started. You can ignore it for the purposes of this tutorial.

In our chat app's messages module, we have two functions you can find in convex/messages.ts:

  • messages:list is a query function, which reads data from your Convex tables.
  • messages:send is a mutation function, which writes data into your Convex tables.

Let's dive into query functions first.

Reading data with query functionsโ€‹

messages:list is a query function that retrieves up to 100 of the most recent documents in the messages table using the ctx.db object provided by Convex:

info

The Convex context object, usually named ctx, provides lots of useful platform capabilities to your cloud functions.

convex/messages.ts
export const list = query({
args: {},
handler: async (ctx) => {
// Grab the most recent messages.
const messages = await ctx.db.query("messages").order("desc").take(100);
// Reverse the list so that it's in a chronological order.
return messages.reverse();
},
});

The query() constructor, imported from Convex, accepts an object with a handler function property that represents the server-side code Convex will run when this query is called. Additionally, an args object can be provided which specifies the arguments expected by the handler.

Exercise

Implement a mini-feature: automatic smileysโ€‹

Let's see how we can edit the code of our functions to enhance our app. Open the project directory in your favorite code editor (VS Code would do), and edit the list query in convex/messages.ts to replace all occurrences of :) with a ๐Ÿ˜€.

See solution

We map over our messages, performing a text replacement on the body of each message.

convex/messages.ts
export const list = query({
args: {},
handler: async (ctx) => {
// Grab the most recent messages.
const messages = await ctx.db.query("messages").order("desc").take(100);
// Reverse the list so that it's in a chronological order.
return messages.reverse().map((message) => ({
...message,
// Format smileys
body: message.body.replaceAll(":)", "๐Ÿ˜Š"),
}));
},
});

Notice when you save your changes to convex/messages.ts, logs will show up in your npm run dev terminal as Convex detects and syncs your changes. You see "Convex functions ready!" when the new version is live.

Immediately, in the chat app, you should see existing messages displayed with emojis, no refresh required.

:) turned into ๐Ÿ˜€
tip

You can write a query directly in the dashboard!

Changing data with mutationsโ€‹

Mutation functions write to the database, letting you create, update, and delete documents. In our case, the send mutation adds a new document to the messages table using ctx.db, which is again provided by Convex to your function:

convex/messages.ts
export const send = mutation({
args: { body: v.string(), author: v.string() },
handler: async (ctx, { body, author }) => {
// Send a new message.
await ctx.db.insert("messages", { body, author });
},
});

Like query(), the mutation() constructor accepts an object with args and handler. But in this case, send requires two arguments: a string with the author name, and a string with the message body. The handler takes these arguments as an object in the second argument to the handler function, immediately after the Convex-provided mutation context.

tip

You can test-run functions in the dashboard with the "Run Function" button. Type in values for the required args and click "Run" to execute the function.

This send mutation doesn't return anything, but the dashboard confirms that the function ran successfully. Back on the "Data" tab, you'll see a new document has been added to the messages table, and you'll see the new message pop up instantly in the chat app.

Running the 'send' function in the dashboard

The new message sent by running 'send' appears instantly in the 'messages' table

Realtime is all the timeโ€‹

You may have noticed you never have to refresh the data view or the app whenever you run a mutation. The new and changed records just appear! This also applies to edits in the dashboard, changes to function source code โ€“ literally any change that would affect your app. Feel free to do lots of random modifications to your app and see for yourself:

This is Convex's reactor at work. It knows precisely when the mutation functions create or modify any records that the query functions depend upon, and it will push new values out as soon as they exist. Even if those query functions aggregate records, or join together multiple tables or whatever.

info

But how? The key is Convex query and mutation TypeScript functions are required to be deterministic. This means they need to return the same value every time they're run when they're given the same arguments and they operate on the same underlying database state. Convex uses this determinism to track the query function "read sets" โ€“ that is, which ranges of records in which tables the function used to produce its result. Since it tracks read sets, it has complete information to determine whether a mutation function made a change that would effect any "subscribed" query. If so Convex automatically re-runs that function and streams the new value to any subscribed clients.

So do I need to be careful about determinism when I write query/mutation functions?

Nope! Convex will give you a helpful error if you rely on any behavior or packages that make your query and mutation functions non-deterministic. You can still use Math.random() and Date.now(), they will behave as you expect.

Some non-determinism, such as calling other cloud APIs, is an crucial part of modern apps. Don't worry, Convex has a great solution for that too! We'll cover it in part three of this tutorial when we integrate OpenAI.

Recapโ€‹

  • Convex stores your data as relational documents organized into tables. The dashboard's Data tab lets you browse tables and view, filter, and edit documents manually.
  • Cloud-hosted TypeScript functions are your programmatic interface to the database. You write these functions on your computer with the rest of your app, Convex keeps them in sync with your backend, and you can view and test-run them in the dashboard's Functions tab.
  • Whenever relevant data changes, every query subscription receives automatic realtime updates via the reactor's data-dependency tracking.

Let's get coding!โ€‹

Phew!

Now that you've gotten your feet wet with the Convex reactor, let's dive into the chat app's code itself and add some cool new features.