Convex 0.13.0 is here and it’s kinda a big deal. The changes in this release generally fall among 2 themes:
- Function arguments: Convex functions now receive a single object as their argument and support validating this object.
- 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
fromconvex/schema
is nowv
inconvex/values
- Breaking:
usePaginatedQuery
API changes - Breaking:
ConvexReactClient
API changes - Breaking: Optimistic update API changes
- Breaking:
ConvexHttpClient
API changes - Breaking:
InternalConvexClient
→BaseConvexClient
and API changes
Actions:
- Actions can run outside of Node.js
- Breaking: Node.js actions need
"use node";
- Breaking:
httpEndpoint
→httpAction
scheduler
in HTTP actions- Node.js actions run in Node.js 18
fetch
in HTTP actionsstorage
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:
- This enabled us to build argument validation (see below). Using named arguments makes it straightforward to tell which validator corresponds to which argument.
- 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: s
→ v
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: InternalConvexClient
→ BaseConvexClient
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 tomutation
.- 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 anoptimisticUpdate
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 ajournal
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: httpEndpoint
→ httpAction
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 aBlob
(not aRequest
).storage.get
now returns aPromise
of aBlob
ornull
(not aResponse
ornull
).
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 withfieldName: null
.q.eq(q.field("fieldName"), undefined)
will only match documents withoutfieldName
.
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:*")
(exrequire("node:fs")
) in Node.js actions. - Convex’s JavaScript environment now supports constructing URLs with
new URL(href, base)
likenew URL("/foo", "<https://convex.dev>")
. - Fixed a number of bugs with
URLSearchParams
in Convex’s JavaScript environment. ConvexReactClient
andBaseConvexClient
now support an additionalreportDebugInfoToConvex
option. If you’re having connection or performance problems, turn this option on and reach out in Discord!- Improved several confusing error messages.