Convex 0.13.0 is here and it’s kinda a big deal. The changes in this release generally fall among 2 themes:

  1. Function arguments: Convex functions now receive a single object as their argument and support validating this object.
  2. Actions: HTTP endpoints and actions are now unified and more powerful.

Along the way we’ve had to make a lot of breaking changes to the platform. Your existing app will continue to function, but the upgrade to 0.13.0 may be involved. Please feel free to reach out in the Convex Discord community if you have questions or need a hand updating your app.

We’re looking to stabilize the API of Convex for 1.0 and plan to make many less breaking changes in the future.

The full list of changes in this release is:

Function Arguments:

  • Breaking: Convex functions receive a single arguments object
  • Argument Validation
  • Breaking: s from convex/schema is now v in convex/values
  • Breaking: usePaginatedQuery API changes
  • Breaking: ConvexReactClient API changes
  • Breaking: Optimistic update API changes
  • Breaking: ConvexHttpClient API changes
  • Breaking: InternalConvexClientBaseConvexClient and API changes

Actions:

  • Actions can run outside of Node.js
  • Breaking: Node.js actions need "use node";
  • Breaking: httpEndpointhttpAction
  • scheduler in HTTP actions
  • Node.js actions run in Node.js 18
  • fetch in HTTP actions
  • storage in actions
  • Breaking: HTTP action storage API changes

Other Changes:

  • Breaking: Querying for documents missing a field
  • Minor Improvements

You can read more about these changes below.

Function Arguments

Breaking: Convex functions receive a single arguments object

Convex queries, mutations, and actions now receive a single object as their argument.

As of version 0.13.0, you can no longer pass multiple arguments to Convex functions. All functions must receive a single object as their argument, or no argument at all.

Previously, you could define functions that took positional arguments like:

export default mutation(async ({ db }, body, author) => {
  //...
});

Now, you define your functions to take an arguments object like:

// `{ body, author }` destructures the single arguments object
// to extract the "body" and "author" fields.
export default mutation(async ({ db }, { body, author }) => {
  //...
});

Similarly, when calling your functions, you must switch from passing in positional arguments like:

const sendMessage = useMutation("sendMessage");
async function onClick() {
  await sendMessage("Welcome to Convex v0.13.0", "The Convex Team");
}

To passing in an arguments object like:

const sendMessage = useMutation("sendMessage");
async function onClick() {
  await sendMessage({
    body: "Welcome to Convex v0.13.0",
    author: "The Convex Team"
  });
}

If your functions don’t need any arguments, no change is needed.

The rationale for this change is two-fold:

  1. This enabled us to build argument validation (see below). Using named arguments makes it straightforward to tell which validator corresponds to which argument.
  2. This enables more API consistency. Now all of the methods for calling Convex functions can have the same (name, arguments, options) API because they don’t have to deal with a variable number of arguments.

The newest versions of the ConvexReactClient, ConvexHttpClient, BaseConvexClient, and Python Convex client all only support calling functions with a single arguments object.

We know that updating your apps to use arguments objects will take work. If you need a hand or want advice on the migration, please join our Discord community. We’re happy to help!

Argument validation

Convex now supports adding optional argument validators to queries, mutations, and actions. Argument validators ensure that your Convex functions are called with the correct types of arguments.

Without argument validation, a malicious user can call your public functions with unexpected arguments and cause surprising results. We recommend adding argument validation to all public functions in production apps.

Luckily, adding argument validation to your functions is a breeze!

To add argument validation, define your functions as objects with args and handler properties:

import { mutation } from "./_generated/server";
import { v } from "convex/values";

export default mutation({
  args: {
    body: v.string(),
    author: v.string(),
  },

  handler: async ({ db }, { body, author }) => {
    const message = { body, author };
    await db.insert("messages", message);
  },
});

The args property is an object mapping argument names to validators. Validators are written with the validator builder, v. This is the same v that is used to define schemas.

The handler is the implementation function.

If you’re using TypeScript, the types of your functions will be inferred from your argument validators! No need to manually annotate your functions with types anymore.

If you don’t want to use Convex’s argument validation, you can continue defining functions via inline functions as before, and validating arguments manually.

To learn more, read the full documentation.

Breaking: sv and moved to convex/values

We’ve renamed the schema builder from s to v and moved it from "convex/schema" to "convex/values". If your project has a schema, it should be updated to look like:

import { defineSchema, defineTable } from "convex/schema";
import { v } from "convex/values";

export default defineSchema({
  messages: defineTable({
    body: v.string(),
    author: v.string(),
  }),
});

We made this change to support argument validation. Now that this builder is shared between schemas and argument validation, it’s more clear to export it from "convex/values".

Breaking: usePaginatedQuery API changes

The API of the usePaginatedQuery React hook has changed to no longer use positional arguments.

To use the new version, define a query function that expects a paginationOpts property in its arguments object like:

import { query } from "./_generated/server";

export default query(async ({ db }, { paginationOpts }) => {
  return await db.query("messages").order("desc").paginate(paginationOpts);
});

You can also include additional properties in the arguments object.

Then you can load the paginated query like:

const { results, status, loadMore } = usePaginatedQuery(
  "listMessages",
  {},
  {
    initialNumItems: 5,
  }
);

To learn more, read the Paginated Queries documentation.

Breaking: ConvexReactClient API changes

We’ve made some tweaks to the mutation and action methods on ConvexReactClient.

Previously they returned callbacks to execute the mutation or action. Now they directly invoke the function and return a Promise of the result.

Calling a mutation now looks like:

const result = await reactClient.mutation("mutationName", { arg: "value" });

Similarly, calling an action now looks like:

const result = await reactClient.action("actionName", { arg: "value" });

Additionally, the mutation method takes in an optional third options parameter that allows you to specify an optimistic update:

const result = await reactClient.mutation(
  "mutationName",
  { arg: "value" },
  {
    optimisticUpdate: (localStore, { arg }) => {
      // do the optimistic update here!
    },
  }
);

The useMutation and useAction hooks are unaffected.

We’ve made these changes for consistency with our other clients.

Breaking: Optimistic update API changes

The API of the methods on OptimisticLocalStore has changed:

  • The second parameter to getQuery is now an arguments object (not an array).
  • The second parameter to setQuery is now an arguments object (not an array).
  • The return value from getAllQueries includes an arguments object (not an array).

This is for consistency with our other APIs for interacting with function results.

Breaking: ConvexHttpClient API changes

We’ve changed the ConvexHttpClient to no longer return callbacks from query, mutation, or action. The new API looks like:

const result = await httpClient.query("queryName", { arg: "value" });
const result = await httpClient.mutation("mutationName", { arg: "value" });
const result = await httpClient.action("actionName", { arg: "value" });

If you haven’t gotten the picture yet, we’re serious about consistency!

Breaking: InternalConvexClientBaseConvexClient and API changes

We’ve renamed InternaConvexClient to BaseConvexClient to make it more clear that this is a foundation for building framework-specific clients.

We’ve also made a couple of API changes for consistency with our other clients:

  • mutate is renamed to mutation.
  • The second parameter to mutation is now an arguments object (not an array).
  • The third parameter to mutation is now an optional options object with an optimisticUpdate field.
  • The second parameter to action is now an arguments object (not an array).
  • The second parameter to subscribe is now an arguments object (not an array).
  • The third parameter to subscribe is now an optional options object with a journal field.

Actions

Actions can run outside of Node.js

Convex 0.13.0 supports running actions within Convex’s custom JavaScript environment in addition to Node.js. Convex’s JavaScript environment is faster than Node.js (no cold starts!) and allows you to put actions in the same files as queries and mutations.

Convex’s JavaScript environment supports fetch and is a good fit for actions that simply want to call a third party API. If you need to use Node.js npm packages, you can still declare that the action should run in Node.js by putting "use node"; at the top of the file.

Here’s an example of colocating an action and an internal mutation in the same file:

import { action, internalMutation } from "./_generated/server";

export default action(async ({ runMutation }, { a, b }) => {
  await runMutation("myAction:writeData", { a });
  // Do other things like call `fetch`.
});

export const writeData = internalMutation(async ({ db }, { a }) => {
  // Write to the `db` in here.
});

To learn more, read the documentation on actions.

Breaking: Node.js actions need "use node";

As mentioned above, if you’d like your action to continue to run in Node.js, you must add "use node;" to the top of the file. Here’s an example:

"use node";
import { action } from "./_generated/server";
import SomeNpmPackage from "some-npm-package";

export default action(async _ => {
  // do something with SomeNpmPackage
});

To minimize errors during migrations, we require all files defined in /convex/actions folder to have “use node”;. The simplest way to migrate is to add this to all relevant files. If you want to try running actions outside of Node.js, move the file outside of the /convex/actions directory, which will default to Convex’s custom JavaScript environment.

Breaking: httpEndpointhttpAction

Convex 0.13.0 is unifying the concepts of “actions” and “HTTP endpoints”. To recap,

  • Actions are functions that can call third party services or use Node.js npm packages.
  • HTTP endpoints are functions used to build an HTTP API.

We’ve decided to make these the same underlying abstraction. In Convex 0.13.0, actions and HTTP endpoints (now called “HTTP actions”) can be run in the same environments and support the same functionality. The only difference is that HTTP actions receive a Request and return a Response whereas action functions receive an arguments object and can return any Convex value.

To migrate your code, replace all usages of httpEndpoint with httpAction.

scheduler in HTTP actions

As part of unifying HTTP actions and actions, we’ve added the scheduler to HTTP actions. Now HTTP actions can directly schedule functions to run in the future:

import { httpAction } from "./_generated/server";

export default httpAction(async ({ scheduler }, request) => {
  await scheduler.runAfter(5000, "fiveSecLaterFunc",);

  return new Response(null, {
    status: 200,
  });
});

Node.js actions run in Node.js 18

We’ve upgraded all Node.js actions to use Node.js 18. This has fetch available automatically.

fetch in HTTP actions

As said above, we’ve added fetch to Convex’s custom JavaScript environment. This means that actions and HTTP actions can call fetch directly without adding "use node;":

import { httpAction } from "./_generated/server";

const postMessage = httpAction(async ({ runMutation }, request) => {
  const data = await fetch("<https://convex.dev>");
  // Do something with data here.

  return new Response(null, {
    status: 200,
  });
});

storage in actions

As part of unifying HTTP actions and actions, we’ve added storage to actions! Now actions can directly upload and retrieve files:

import { action } from "./_generated/server";

export default action(({ storage }) => {
  const blob = new Blob(["hello world"], {
    type: "text/plain",
  });
  return await storage.store(blob);
});

Breaking: HTTP action storage API changes

We’ve made a few API changes to storage in HTTP actions:

  • storage.store now receives a Blob (not a Request).
  • storage.get now returns a Promise of a Blob or null (not a Response or null).

The rationale for this change is that we want to expose the same storage API in action functions and HTTP actions. Given that action functions don’t necessary have a Request already, Blob is a more natural type for these methods to use.

Other Changes

Breaking: Querying for documents missing a field

Previously, if you wanted to write a database query for documents without an author field, you could query like:

const messagesWithoutAuthors = await db
  .query("messages")
  .filter(q => q.eq(q.field("author"), null))
  .collect();

This would match documents without an author like { message: "hello" }. as well as documents with a null author like { message: "hello", author: null }.

In version 0.13.0, you should instead compare the field to undefined to find documents that are missing a field:

const messagesWithoutAuthors = await db
  .query("messages")
  .filter(q => q.eq(q.field("author"), undefined))
  .collect();

Currently, the behavior is the same (it will include documents missing author and documents with a null author).

On May 15, 2023, this behavior will change. At this point:

  • q.eq(q.field("fieldName"), null) will only match documents with fieldName: null.
  • q.eq(q.field("fieldName"), undefined) will only match documents without fieldName.

This same change also applies to filters within index range expressions and search expressions.

If you never filter for documents without a field, no change is needed.

The rationale for this change is that we’d like to enable developers to differentiate documents that have a field set to null from documents that are missing the field. They are different in JS so we should allow you to query for them separately.

Minor Improvements

  • Added support for imports like require("node:*") (ex require("node:fs")) in Node.js actions.
  • Convex’s JavaScript environment now supports constructing URLs with new URL(href, base) like new URL("/foo", "<https://convex.dev>").
  • Fixed a number of bugs with URLSearchParams in Convex’s JavaScript environment.
  • ConvexReactClient and BaseConvexClient now support an additional reportDebugInfoToConvex option. If you’re having connection or performance problems, turn this option on and reach out in Discord!
  • Improved several confusing error messages.