1: The reactor
You are here
Convex Tour
- The reactor
Learn about Convex's reactive database accessed through TypeScript cloud functions
- Convex and your app
- The platform in action
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
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.
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:
The Convex context object, usually named ctx
, provides
lots of useful platform capabilities
to your cloud functions.
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.
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.
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 ๐
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:
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.
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.
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.
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.