Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Typed Convex clients for Rust

CONVEX-TYPEGEN · V1 DOCUMENTATION

convex-typegen gives Rust the end-to-end type safety TypeScript gets from Convex. Your schema and function signatures become Rust types at build time, so a wrong table, field, argument, or return type is a compile error—not a runtime bug. Schema changes break the Rust build until your code matches.

Build your first project  ·  What's guaranteed and what isn't

Without and with generated types

Official SDK only — string paths, BTreeMap arguments, manual Value decoding:

use std::collections::BTreeMap;

use convex::{ConvexClient, FunctionResult, Value};

let mut client = ConvexClient::new(&url).await?;
let result = client.query("tasks:list", BTreeMap::new()).await?;
let texts: Vec<String> = match result {
    FunctionResult::Value(Value::Array(items)) => items
        .into_iter()
        .filter_map(|item| match item {
            Value::String(text) => Some(text),
            _ => None,
        })
        .collect(),
    _ => return Err("unexpected query result".into()),
};

With convex-typegen — typed paths, args, and returns (same tutorial as first project):

use convex::ConvexClient;
use convex_api::api::tasks;
use convex_typegen_runtime::TypedClient;

let mut sdk = ConvexClient::new(&url).await?;
let mut client = TypedClient::new(&mut sdk);
let texts: Vec<String> = client.query(tasks::list, tasks::ListArgs {}).await?;

The generator reads local TypeScript or a committed function spec at build time. The runtime uses the generated references to encode your arguments and decode Convex results, including IDs, documents, and application errors.

Rust support: 1.97 for the generator; 1.85 for the runtime with pregenerated bindings. See how generation works for the crate split.

Build your first project

Build a task list that writes a row and observes it through a live query. By the end you will have called all three Convex function kinds and handled a typed application error.

This tutorial is the shortest path to end-to-end type safety: after generation, tasks::list expects ListArgs, returns Vec<String>, and tasks::add returns Id<tables::Tasks>—the compiler rejects the wrong reference, args, or result type before you deploy. See what’s guaranteed and what isn’t for compile-time limits and runtime checks.

The generator reads your schema and local TypeScript or JSON and stays offline during builds. It requires Rust 1.97; the runtime supports 1.85.

1. Create the project

Install Rust 1.97 or newer, Node.js 22 or newer with npm, and Docker. Node and Docker are for the backend setup and deployment. Cargo can generate and compile the bindings without either.

cargo new --edition 2024 typed-tasks
cd typed-tasks
mkdir convex

Replace the empty [dependencies] section of Cargo.toml with the two sections below. The two typegen crates must have matching release versions. These instructions target v1; until v1 is published, use path dependencies pointing at the repository root and its runtime/ directory.

[dependencies]
convex = "0.10.4"
convex-typegen-runtime = "1"
futures = "0.3"
tokio = { version = "1.52.3", features = ["macros", "rt-multi-thread", "time"] }

[build-dependencies]
convex-typegen = "1"

Copy this package.json to the project root. The Convex CLI ships with the Convex npm package.

{
  "name": "convex-typegen-tutorial",
  "private": true,
  "dependencies": {
    "convex": "1.49.0"
  },
  "devDependencies": {
    "typescript": "5.9.3"
  }
}

2. Declare the Convex schema and functions

Create convex/schema.ts. The optional priority can be omitted, set to null, or set to one of two strings. Rust preserves all three cases.

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

export const priority = v.nullable(v.union(v.literal("low"), v.literal("high")));

export default defineSchema({
  tasks: defineTable({ text: v.string(), priority: v.optional(priority) }),
});

Create convex/tasks.ts. Each endpoint declares both validators, even when the argument object is empty. The Convex CLI generates _generated/server during deployment; Rust generation can recognize the import without that generated directory.

import { action, mutation, query } from "./_generated/server";
import { ConvexError, v } from "convex/values";
import { priority } from "./schema";

export const list = query({
  args: {},
  returns: v.array(v.string()),
  handler: async (ctx) =>
    (await ctx.db.query("tasks").collect()).map((task) => task.text),
});

export const add = mutation({
  args: { text: v.string(), priority: v.optional(priority) },
  returns: v.id("tasks"),
  handler: async (ctx, args) => {
    if (!args.text.trim()) {
      throw new ConvexError({ code: "empty_text" });
    }
    return await ctx.db.insert("tasks", args);
  },
});

export const ping = action({
  args: {},
  returns: v.string(),
  handler: async () => "pong",
});

To enable TypeScript validation on deployment, also create convex/tsconfig.json:

{
  "compilerOptions": {
    "target": "ESNext",
    "module": "ESNext",
    "moduleResolution": "Bundler",
    "strict": true,
    "noEmit": true,
    "skipLibCheck": true
  },
  "include": ["./**/*.ts"],
  "exclude": ["./_generated"]
}

3. Generate and use the bindings

Create build.rs next to Cargo.toml:

fn main()
{
    convex_typegen::build::generate_out_dir().expect("generate Convex bindings");
}

The build script reads the local validators and writes convex_api.rs into Cargo’s OUT_DIR. Add src/main.rs in stages, then use the complete file below.

Connect and list

Create the Convex Rust client, wrap it with TypedClient, and run the tasks:list query:

let mut sdk = ConvexClient::new(&std::env::var("CONVEX_URL")?).await?;
let mut client = TypedClient::new(&mut sdk);
let before: Vec<String> = client.query(tasks::list, tasks::ListArgs {}).await?;

Reject bad input and call an action

Use with_error for a typed application error payload. Send priority: None to omit the optional field on the wire. Run a mutation and an action the same way—pass the generated reference and matching args struct.

Subscribe after a write

Open a subscription on tasks:list, insert a row with high priority (Some(Some(tasks::AddArgsPriority::High))), and wait until the stream reports the new text. See typed calls.

Complete main.rs

#[allow(dead_code)]
mod convex_api
{
    convex_typegen_runtime::include!();
}

use std::time::{Duration, SystemTime, UNIX_EPOCH};

use convex::ConvexClient;
use convex_api::api::tasks;
use convex_api::tables;
use convex_typegen_runtime::model::Id;
use convex_typegen_runtime::{CallError, ConvexValue, TypedClient};
use futures::StreamExt;
use tokio::time::timeout;

#[derive(Debug, ConvexValue)]
struct AddFailure
{
    code: String,
}

#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>>
{
    let mut sdk = ConvexClient::new(&std::env::var("CONVEX_URL")?).await?;
    let mut client = TypedClient::new(&mut sdk);

    let before: Vec<String> = client.query(tasks::list, tasks::ListArgs {}).await?;
    println!("Before: {before:?}");

    let reply: String = client.action(tasks::ping, tasks::PingArgs {}).await?;
    println!("Action: {reply}");
    assert_eq!(reply, "pong");

    let add = tasks::add.with_error::<AddFailure>();
    let invalid = tasks::AddArgs {
        text: String::new(),
        priority: None,
    };
    match client.mutation(add, invalid).await {
        Err(CallError::Application { data: Ok(failure), .. }) => {
            assert_eq!(failure.code, "empty_text");
            println!("Rejected: {}", failure.code);
        }
        Err(other) => return Err(other.into()),
        Ok(_) => return Err("the backend accepted empty text".into()),
    }

    // ANCHOR: subscribe
    let mut updates = client.subscribe(tasks::list, tasks::ListArgs {}).await?;
    let text = format!("Write Rust ({})", SystemTime::now().duration_since(UNIX_EPOCH)?.as_nanos());
    let id: Id<tables::Tasks> = client
        .mutation(
            add,
            tasks::AddArgs {
                text: text.clone(),
                priority: Some(Some(tasks::AddArgsPriority::High)),
            },
        )
        .await?;
    println!("Created: {}", id.as_str());

    // The stream can first yield the initial result. Wait for this write to appear.
    timeout(Duration::from_secs(10), async {
        while let Some(update) = updates.next().await {
            if update?.contains(&text) {
                println!("Observed: {text}");
                return Ok::<(), Box<dyn std::error::Error>>(());
            }
        }
        Err("subscription ended before observing the write".into())
    })
    .await??;
    // ANCHOR_END: subscribe

    Ok(())
}

Run cargo check now. Cargo may fetch Rust dependencies the first time; generation itself reads only local files. The function references tell the compiler which kind of call to make and which arguments and result to expect. For example, tasks::add takes AddArgs and returns an Id<tables::Tasks>. See generated models for the three priority shapes.

4. Deploy and run locally

Start the self-hosted Convex backend with Docker. This example uses a disposable container on loopback:

docker run --detach --name typed-tasks-convex \
  --publish 127.0.0.1:3210:3210 --publish 127.0.0.1:3211:3211 \
  --env CONVEX_CLOUD_ORIGIN=http://127.0.0.1:3210 \
  --env CONVEX_SITE_ORIGIN=http://127.0.0.1:3211 \
  --env DISABLE_BEACON=true \
  ghcr.io/convex/convex-backend@sha256:e9c084e8bd6c1f249012c672193f0988c748824f55a24cdad45f6185a8462f9a

Once curl -f http://127.0.0.1:3210/version succeeds, generate a deployment admin key and deploy the functions from the project root:

export CONVEX_SELF_HOSTED_URL=http://127.0.0.1:3210
export CONVEX_SELF_HOSTED_ADMIN_KEY="$(docker exec typed-tasks-convex ./generate_admin_key.sh | tail -n 1)"
npm install
npx convex dev --once --typecheck enable
unset CONVEX_SELF_HOSTED_ADMIN_KEY
CONVEX_URL=http://127.0.0.1:3210 cargo run --bin typed-tasks

On a fresh database, expect output like this (the ID and timestamp will differ):

Before: []
Action: pong
Rejected: empty_text
Created: <task id>
Observed: Write Rust (<timestamp>)

The mutation writes the task, and the subscription delivers a new list containing it. Running again starts with the previous rows. When finished, remove the disposable container with docker rm -f typed-tasks-convex. See deploy and connect for environment and authentication details, or troubleshooting if the output differs.

What’s guaranteed and what isn’t

convex-typegen exists so Rust callers get the same compile-time contract TypeScript gets from Convex’s generated api and Doc types. The types reflect whatever schema and validators you feed the generator at build time. They do not replace Convex’s runtime rules.

Guaranteed at compile time

When you use generated references and models with TypedClient, the Rust compiler checks:

  • Function paths — pass tasks::list to query, not mutation, and never a mistyped string path on the typed API.
  • Argument shapes — each endpoint’s args struct matches its Convex validators (required fields, optionals, nested objects, literal enums).
  • Return types — declared returns validators become Rust result types (for example Vec<String> for tasks::list, Id<tables::Tasks> for tasks::add).
  • Tables and documents — tables::* models, Id<T>, and document fields line up with convex/schema.ts.
  • Optionals vs null — v.optional and nullable unions encode as distinct Rust types where the generator models them.
  • Application errors — with_error::<YourPayload>() ties a declared error shape to decoding via CallError.

Schema or function changes that alter generated types fail the Rust build until you regenerate bindings and update call sites—similar to TypeScript after npx convex dev.

Escape hatches by design

Not every call has to go through generated types:

  • client_mut() on TypedClient returns &mut convex::ConvexClient for auth, watch_all, and untyped query / mutation / action using path() plus raw Convex values.
  • Re-exported convex crate — convex_typegen_runtime::convex is the SDK version the runtime targets; you can use it directly alongside generated code.

Choosing an escape hatch opts out of compile-time checking for that call.

Deploy drift

Generated types describe the contract when you last built. If you deploy a changed backend without regenerating and rebuilding the Rust app, the compiler cannot catch the mismatch; Convex may reject calls at runtime (wrong args, missing fields, changed return shapes)—the same class of problem as running an old TypeScript client against a new deployment.

Practice: regenerate bindings and rebuild Rust whenever validators or the schema change, and treat binding diffs in review like TypeScript _generated updates.

Runtime-only checks

Rust types cannot prove:

  • Whether an Id still exists — deleting or never inserting a row is validated when the query or mutation runs.
  • Auth and permissions — tokens, roles, and deployment rules are enforced on the server.
  • Convex validators beyond your declared contract — internal functions, custom server checks, and data written outside your schema still execute at runtime.

For a runnable contrast between typed calls and SDK escape hatches, see typed calls and subscriptions.

How generation works

Convex functions run in the backend. A Rust consumer builds bindings for their validators locally, then connects to a deployment at runtime.

  1. Describe the wire contract. Write the table schema and declare each public function’s args and returns. The generator reads the schema and either the function sources or a committed function spec.
  2. Generate at build time. The convex-typegen build dependency writes Rust code. The default Cargo helper writes to OUT_DIR and registers inputs so Cargo knows when to rerun it.
  3. Include and use the types. The convex-typegen-runtime application dependency provides include!(), the wire codec, and TypedClient. The client adapts the official Convex SDK. Add convex as a direct dependency when your code imports the SDK.

Versions and Rust versions

Use the same release of the generator and runtime. Building the generator requires Rust 1.97. If you pregenerate the bindings and omit the build dependency, the runtime supports Rust 1.85. The Rust API and generated names follow the semantic versioning policy.

Path-scoped names

A public list in convex/admin/users.ts becomes api::admin::users::list, with its arguments and return type in the same module. The reference carries the wire path admin/users:list, so there is no function-name string to keep in sync in your Rust calls. See generated model naming.

Choose an input and an output independently

TypeScript function sources work well when validators can be resolved statically. Choose a function spec when you need the deployment’s resolved validators or want to commit a snapshot of that interface. The schema always comes from TypeScript.

OUT_DIR works well when Cargo should regenerate bindings on each relevant change. Choose checked-in output when you want code review to show binding changes or when your consumers build without the generator. Neither output choice requires a deployment during generation.

Typed calls and subscriptions

The runtime wraps the official Convex Rust client with generated function references. These references constrain queries, mutations, and actions to the matching argument and return models, expose query subscriptions as streams, and can declare a Rust type for application error payloads.

Use TypedClient when the generated Rust types should validate arguments and decode results for you. Create the official SDK client first and give the adapter a mutable borrow. Use the first project for a runnable consumer with the functions below.

Calls

OperationUse it whenTutorial example
queryYou need one result from a read.client.query(tasks::list, tasks::ListArgs {}).await?
mutationYou need a transactional write and its result.client.mutation(tasks::add, args).await?
actionYou need to call a backend action, for example one that talks to another service.client.action(tasks::ping, tasks::PingArgs {}).await?

Pass the generated function reference as the first argument and its matching args as the second. The Rust compiler rejects a query reference passed to mutation, or args from a different endpoint. A declared empty args object still has its own type: pass tasks::ListArgs {} to tasks::list. A declared return validator determines the result type. See generated models for undeclared validators.

Like the SDK, query opens a subscription, returns its first result, and then drops the subscription.

Subscriptions

Use subscribe when a view should stay current as data changes. Only queries can be subscribed to. The tutorial opens the stream, writes a new row, and waits for the write to appear in a result. futures::StreamExt provides next().

    let mut updates = client.subscribe(tasks::list, tasks::ListArgs {}).await?;
    let text = format!("Write Rust ({})", SystemTime::now().duration_since(UNIX_EPOCH)?.as_nanos());
    let id: Id<tables::Tasks> = client
        .mutation(
            add,
            tasks::AddArgs {
                text: text.clone(),
                priority: Some(Some(tasks::AddArgsPriority::High)),
            },
        )
        .await?;
    println!("Created: {}", id.as_str());

    // The stream can first yield the initial result. Wait for this write to appear.
    timeout(Duration::from_secs(10), async {
        while let Some(update) = updates.next().await {
            if update?.contains(&text) {
                println!("Observed: {text}");
                return Ok::<(), Box<dyn std::error::Error>>(());
            }
        }
        Err("subscription ended before observing the write".into())
    })
    .await??;

The stream delivers an initial result and then reflects changes to the data read by the query. Each item decodes independently. Keep the SDK client alive while polling the stream; the stream owns the subscription and releases it on drop. A closed subscription ends the stream. In an integration check, bound the wait and look for the expected change rather than assuming it is the next item; the tutorial does both.

See application errors to distinguish application failures from encoding, decoding, and transport failures.

Using the rest of the Convex SDK

TypedClient borrows a &mut convex::ConvexClient. Use its typed methods for generated calls and client_mut() whenever you need the SDK directly. Set up the connection and make any SDK clones before borrowing it.

Import the SDK with use convex_typegen_runtime::convex; to use the exact version and types the runtime uses. See the semver policy and the Convex Rust SDK docs.

SDK capabilityWhere to use itExample
Read once with queryTyped methodclient.query(tasks::list, tasks::ListArgs {}).await?
Write with mutationTyped methodclient.mutation(tasks::add, args).await?
Run an actionTyped methodclient.action(tasks::ping, tasks::PingArgs {}).await?
Follow one query with subscribeTyped methodclient.subscribe(tasks::list, tasks::ListArgs {}).await?
Set or clear a user tokenclient_mut()client.client_mut().set_auth(Some(token)).await; Pass None to sign out.
Fetch fresh tokens with set_auth_callbackclient_mut()client.client_mut().set_auth_callback(Some(fetch_token)).await; See the callback type below.
Authenticate as a deployment adminclient_mut()client.client_mut().set_admin_auth(admin_key, None).await;
Watch all active queries with watch_allclient_mut()let mut all = client.client_mut().watch_all(); This gives you the SDK’s QuerySetSubscription. Use all.next().await with futures::StreamExt.
Make untyped callsclient_mut()client.client_mut().query(tasks::list.path(), raw_args).await? See how to encode raw_args below.
Connect with default optionsBefore wrappinglet mut sdk = convex::ConvexClient::new(url).await?;
Configure client ID or connection-state notificationsBefore wrappingconvex::ConvexClientBuilder::new(url).with_client_id("tasks").with_on_state_change(state_tx)
Connect with a builderBefore wrappinglet mut sdk = convex::ConvexClient::new_from_builder(builder).await?; You can also call builder.build().await?.
Share a connection across SDK handlesBefore wrappinglet mut other_sdk = sdk.clone(); Give each adapter its own mutable borrow.

The SDK calls an auth callback with force_refresh = true on reconnect. Pass that flag to your token provider so it can fetch a fresh token. For example, if auth_provider is an Arc to your provider with an async token(force_refresh) method returning a JWT:

let fetch_token: convex::AuthTokenFetcher = Box::new(move |force_refresh| {
    let provider = auth_provider.clone();
    Box::pin(async move {
        let jwt = provider.token(force_refresh).await?;
        Ok(convex::AuthenticationToken::User(jwt))
    })
});
client.client_mut().set_auth_callback(Some(fetch_token)).await;

Use the generated function’s path() and encode its args as a Convex object for an untyped SDK call. The SDK returns a FunctionResult, so you can decode a successful value separately or inspect the SDK’s error variants.

use convex::{FunctionResult, Value};
use convex_typegen_runtime::value_serde::{from_value, to_value};

let Value::Object(raw_args) = to_value(&tasks::ListArgs {})? else {
    return Err("expected a Convex argument object".into());
};
let result = client.client_mut().query(tasks::list.path(), raw_args).await?;
if let FunctionResult::Value(value) = result {
    let tasks: Vec<String> = from_value(value)?;
    println!("{tasks:?}");
}

A builder accepts a Tokio channel sender for connection-state updates. Keep the receiver wherever your app handles connection status.

use convex::{ConvexClient, ConvexClientBuilder};
use convex_typegen_runtime::TypedClient;

let (state_tx, mut state_rx) = tokio::sync::mpsc::channel(8);
let builder = ConvexClientBuilder::new(url)
    .with_client_id("tasks")
    .with_on_state_change(state_tx);
let mut sdk = ConvexClient::new_from_builder(builder).await?;
let mut other_sdk = sdk.clone();
let mut client = TypedClient::new(&mut sdk);
let mut other_client = TypedClient::new(&mut other_sdk);

Handle application errors

Use a thrown ConvexError for a failure your app can act on, such as an invalid form submission. In the task tutorial, the tasks:add mutation throws new ConvexError({ code: "empty_text" }) for a blank title.

Decode a payload

Function validators and specs do not describe thrown payloads. Generated references use the SDK’s convex::Value for application error data by default. Declare a known payload shape using with_error::<T>(), where T implements owned deserialization. The tutorial uses a handwritten AddFailure with #[derive(ConvexValue)], calls client.mutation(tasks::add.with_error::<AddFailure>(), invalid), and matches Err(CallError::Application { data: Ok(failure), .. }) to read failure.code.

The override applies to calls and subscription items that use that reference. Use raw_errors() on a typed reference to return to raw SDK payloads, for example when debugging an endpoint with multiple payload shapes.

Choose a response

FailureWhat it means
Application { data: Ok(payload), .. }The server threw ConvexError and its payload decoded. Use the payload to decide what the app should do.
Application { data: Err(invalid), .. }The payload did not match the Rust type. Inspect invalid.raw and the path-aware invalid.source; the original failure is still an application error.
Encode { .. }Arguments could not be encoded. Check your arguments and wire types.
Decode { .. }A successful response did not match the expected return type. Check that your bindings and deployment agree.
Transport { .. }The SDK could not complete the request or establish the subscription. Check the deployment URL and connection.
Server { .. }Convex returned a plain error message without an application payload. Check the backend logs.

CallError is non-exhaustive. Use a fallback arm when matching it and a struct pattern with .. for each variant. To bind a tuple variant’s payload, use a numeric field, as in CallError::Transport { 0: error, .. }. The transport payload is a TransportError. Use its std::error::Error::source() to inspect the underlying error chain. Avoid logging raw payloads that may contain user data.

Generated models

Codegen turns your Convex schema and function validators into Rust structs and enums. Each model derives ConvexValue and round-trips through convex_typegen_runtime::value_serde::{to_value, from_value} so Int64, Float64, bytes, and nested objects keep their wire kinds.

Args, documents, and string enums

When convex/tasks.ts exports an add mutation with { text: v.string(), … }, you get a typed args struct next to the function reference:

client.mutation(
    tasks::add,
    tasks::AddArgs {
        text: "Ship it".into(),
        priority: None,
    },
);

A query that returns table rows yields document structs with _id, _creation_time, and your fields. IDs use model::Id<tables::Tasks>; full rows use model::Doc<tables::Tasks>.

A union of string literals becomes a Rust enum with one variant per string. Validator v.union(v.literal("low"), v.literal("high")) becomes AddArgsPriority with Low and High variants that encode as "low" and "high" on the wire. String-literal enums implement Copy, PartialEq, Eq, and Hash, including when wrapped in Option.

Generated structs and other union enums implement PartialEq. They also implement Eq when all fields or variant payloads implement it. An f64 or RawValue anywhere in a nested field prevents Eq. For example, document models have an f64 creation time, while an Id<T> still implements Eq.

Function references

Public queries, mutations, and actions become constants under api. The list export in convex/tasks.ts is tasks::list; its path() returns "tasks:list". Each reference implements FunctionReference with associated Args, Returns, Kind, and application Error types. See typed calls and application errors.

Module segments and exports use snake_case. Internal functions, HTTP actions, crons, and modules outside your configured Convex root are omitted from api.

Shorter Rust names, same Convex paths

Rust types drop a repeated prefix when it matches the enclosing module name. The action integrations:integrationsMirror still uses that Convex path, but the generated args struct is api::integrations::MirrorArgs and the reference is api::integrations::mirror. Struct fields stay snake_case in Rust; Serde keeps the original wire names (camelCase, quoted keys, and _creationTime on documents).

Nested validator shapes become nested Rust types named after their field or container role.

IDs, documents, and unions

model::Id<tables::Tasks> is a string on the wire with a distinct Rust type per table. The backend still validates the ID string against the table you used at compile time.

model::Doc<tables::Tasks> is the generated row type. Union tables emit a Rust enum with one variant per branch; each branch carries its own _id and _creation_time fields.

Optional and null fields

v.optional(T) maps to Option<T>. None omits the key on encode. When the key is present, the value must decode as T.

For a nullable union of string literals (including v.nullable around literals), use a plain enum for the strings and Option<Enum> on a required field:

  • Some(AddArgsPriority::High) sends "high".
  • None on a required nullable field sends JSON null.

When the validator is both optional and nullable, the outer Option tracks whether the field was sent:

  • None — key omitted.
  • Some(None) — key present with JSON null.
  • Some(Some(AddArgsPriority::High)) — key present with a string literal.

For other nullable validators, v.nullable(T) becomes a union of T and () for Convex null. Optional fields use None for absence and Some(...) when the key is present, wrapping the inner null branch when the value is explicitly null.

Other union enums carry each branch’s model. When every branch shares a required field with a distinct literal tag, decoding picks the branch from that field. Otherwise branches are tried in declaration order. Some branches share wire shapes (for example IDs from different tables); the backend validates IDs.

Literal return types become unit structs or fixed numeric types. Records keep string-shaped keys. Bytes use the runtime Bytes wrapper.

Set, clear, or omit a priority

The first-project tutorial declares priority: v.optional(v.nullable(v.union(v.literal("low"), v.literal("high")))) on tasks:add. Use these values for the generated AddArgs::priority field:

IntentRust valueConvex object
Leave the field unspecifiedNoneNo priority key
Send an explicit empty prioritySome(None){ priority: null }
Assign high prioritySome(Some(tasks::AddArgsPriority::High)){ priority: "high" }

The tutorial sends None for the rejected empty-text mutation and Some(Some(tasks::AddArgsPriority::High)) for the successful write. All three states round trip through the runtime codec. For a patch endpoint, your handler can treat omission and null differently; the generator preserves presence on the wire.

Raw values when validators are missing

Some endpoints do not declare args or returns in the spec. You can still call them:

SituationGenerated typeWhat to do
No args validatorfunction::RawArgs (BTreeMap<String, RawValue>)Build a map or use RawArgs::new() for {}
Empty args object validatorEmpty endpoint-specific structPass MyArgs {}
No return validatormodel::RawValueDecode manually or narrow with from_value
v.any()model::RawValueSame as above; wire kinds are preserved

If TypeScript builds validators dynamically, static generation may not see the full shape. Capture a function spec from a deployed backend and commit it—see generation. The spec carries validators the offline parser cannot resolve.

Reference: literal and variant names

You rarely need these rules day to day. They explain odd variant names in diffs and snapshots.

String-literal enums use PascalCase variant names derived from ASCII letters and digits. Punctuation splits words. Leading digits and the name Self get a Type prefix. Literals with no ASCII letters or digits become _Wire plus lowercase UTF-8 hex bytes (for example _Wire21 for "!").

When two variants would share a name, each gets a Wire suffix plus the original string’s lowercase UTF-8 hex bytes. Extra trailing X characters avoid colliding with an existing base name. Reordering literals in TypeScript does not rename variants.

If two different Convex exports would collide in the same Rust module, the generator reports both origins so you can rename the TypeScript export or adjust modules.

Macros

Use ConvexValue for Rust types you write yourself—application error payloads, patch objects, or v.any() results you decode by hand. Generated models already derive it; you only need the macro when you add your own structs or enums.

Convert between your types and SDK values with the runtime codec:

use convex::Value;
use convex_typegen_runtime::{ConvexValue, value_serde::{from_value, to_value}};

#[derive(ConvexValue)]
struct Patch {
    title: String,
}

let wire: Value = to_value(&Patch { title: "Draft".into() })?;
let patch: Patch = from_value(wire)?;

The derive implements Serde with Convex wire rules. Pair it with to_value / from_value rather than serde_json when talking to convex::Value.

Include generated bindings

Use the runtime’s include!() macro to load generated Rust in a module after Cargo runs the build script. See include bindings from Cargo output for the default and custom filename forms.

Derive a Convex model

Use ConvexValue when you control a wire shape outside the generated validators, for example an application error payload, an adapter around v.any(), or a patch model. For an arbitrary result represented by RawValue, pass its inner SDK value (raw.0) to from_value::<YourModel>(raw.0) once you know its shape.

The example below encodes a patch into an SDK Value, checks the exact wire keys and values, and decodes it back. Use the tutorial manifest, save the file as src/bin/wire.rs, and run cargo run --bin wire. No backend is needed; success means all assertions pass. The runtime supplies the derive and the codec, so this example needs no direct Serde dependency.

use std::collections::BTreeMap;

use convex::Value;
use convex_typegen_runtime::ConvexValue;
use convex_typegen_runtime::value_serde::{from_value, to_value};

#[derive(Debug, PartialEq, ConvexValue)]
enum Priority
{
    #[convex(literal = "low")]
    Low,
    #[convex(literal = "high")]
    High,
}

#[derive(Debug, PartialEq, ConvexValue)]
struct TaskPatch
{
    #[convex(rename = "taskText")]
    text: String,
    #[convex(optional)]
    priority: Option<Option<Priority>>,
}

fn main() -> Result<(), Box<dyn std::error::Error>>
{
    for (priority, wire_priority) in [
        (None, None),
        (Some(None), Some(Value::Null)),
        (Some(Some(Priority::High)), Some(Value::String("high".into()))),
    ] {
        let patch = TaskPatch {
            text: "Write Rust".into(),
            priority,
        };
        let mut expected = BTreeMap::from([("taskText".into(), Value::String("Write Rust".into()))]);
        if let Some(value) = wire_priority {
            expected.insert("priority".into(), value);
        }

        // Encode an SDK value, then decode it back into the handwritten model.
        let wire = to_value(&patch)?;
        assert_eq!(wire, Value::Object(expected));
        assert_eq!(from_value::<TaskPatch>(wire)?, patch);
    }
    Ok(())
}

Supported attributes and shapes

  • Named-field structs encode as objects and reject unknown fields on decode. Field names default to the Rust spelling, with the r# prefix removed.
  • Use #[convex(optional)] on Option<T> to omit None and decode a present key as T. An unmarked Option<T> is required and nullable. An optional field can also accept null when its inner type accepts null, such as the inner Option<Priority> above.
  • Use i64 for Int64, f64 for Float64, Bytes for bytes, and BTreeMap<K, V> with string-shaped keys for records.
  • Use a unit struct with #[convex(literal = "created")] for an exact wire literal. Booleans, signed 64-bit integers, and floats also work. Write 1 or 1_i64 for Int64 and 1.0 for Float64. The generator can use #[convex(float_bits = ...)] to preserve an exact Float64 bit pattern.
  • For a string literal enum, annotate each unit variant with a distinct #[convex(literal = "...")] string. The enum encodes as that exact string.
  • Tuple variants encode the payload of a single-field tuple variant directly. Decoding tries variants in declaration order. For a discriminated union, set #[convex(discriminator = "kind")] on the enum and #[convex(tag = CreatedTag)] on each variant, using its distinct literal type for the required kind field. The decoder selects the branch by that wire field and validates the whole payload.

The derive supports concrete types. Tuple structs, Rust unions, and variants with unsupported field shapes produce compile-time diagnostics. For generated function calls, use the typed references under bindings::api.

Why generation stays in the build script

The derive sees only the Rust item it annotates. The generator reads TypeScript or a committed function spec offline. Keeping those inputs in build.rs lets Cargo rebuild when they change and gives rust-analyzer a generated Rust file to navigate. Neither macro runs generation or touches the network.

Choose generation inputs and output

Use a custom layout or monorepo

Keep the Rust package’s build script and point it at the backend root. For example, if the package is apps/desktop and the Convex project is apps/backend:

use convex_typegen::{build, config::Configuration};

fn main() {
    build::generate(Configuration::default().with_convex_dir("../backend/convex"))
        .expect("generate Convex bindings");
}

This reads ../backend/convex/schema.ts, derives module names relative to ../backend/convex, and leaves the output in Cargo’s OUT_DIR. An explicit with_schema_path takes precedence if your schema lives elsewhere. Use with_function_paths if you only want a known subset of source files.

Use a committed function spec

Choose a spec when the source parser cannot resolve a function’s validators, or when you want a reviewable snapshot of the deployed interface. After deploying to your local self-hosted backend with both CONVEX_SELF_HOSTED_* variables set, run this from the backend project root:

npx convex function-spec > function-spec.json.tmp &&
  mv function-spec.json.tmp function-spec.json

Review and commit the JSON. In the preceding build script, add .with_function_spec_path("../backend/function-spec.json") after with_convex_dir. The spec replaces TypeScript function discovery entirely, including any explicit function paths. Table schemas still come from TypeScript. Refresh the spec when your deployed function inventory or validators change. Run deployment and capture as separate steps; the build script does not need credentials, the Convex CLI, or a running backend.

Commit bindings and check for drift

Use this when downstream consumers should compile without the generator, or when you want to review binding changes in Git. Install the CLI for your matching release (cargo install convex-typegen --version 1 --locked). During development of this repository, use cargo install --path /path/to/convex-typegen --locked instead.

From the consumer package root, generate once, then run the check in CI:

convex-typegen --output src/convex_api.rs
convex-typegen --check --output src/convex_api.rs

Pass the same input flags to both invocations. Use mod convex_api; to load the checked-in file from src/. Remove the generation build script and its build dependency if this is the only generated output. Keep the matching runtime dependency. The drift check reads local inputs and compares bytes without writing; see CLI exit codes.

Include bindings from Cargo output

For the default output, put convex_typegen_runtime::include!(); inside the module that should own the generated items, as in the first project. If you need a different filename under OUT_DIR, use .with_out_file(PathBuf::from(std::env::var_os("OUT_DIR").expect("Cargo OUT_DIR")).join("bindings.rs")) in the build script (import std::path::PathBuf). Then use convex_typegen_runtime::include!("bindings.rs"); inside your Rust module. The macro takes a string literal relative to OUT_DIR; it just includes Rust source.

Deploy and connect

Deploy the TypeScript backend separately

Use the local self-hosted setup for the tutorial. The Convex CLI uses CONVEX_SELF_HOSTED_URL and CONVEX_SELF_HOSTED_ADMIN_KEY to deploy the functions and optionally capture the function spec. Keep the key in your deployment environment and out of source control.

Cargo builds your Rust program from local TypeScript and optional spec JSON. Deploying a backend does not regenerate an existing Rust binary, and compiling Rust does not deploy the TypeScript. After changing validators, deploy and rebuild so both sides use the same contract.

Connect at runtime

Pass the deployment’s client URL to convex::ConvexClient::new. The tutorial reads CONVEX_URL itself. Use the client API URL (port 3210 in the tutorial); the separate site port is for HTTP actions.

Public endpoints may still require authentication. Set the user token with the official SDK’s set_auth before calls or subscription setup. The adapter’s client_mut() gives you access to the SDK while it is borrowed. The local tutorial endpoints do not require auth. Keep deployment admin credentials out of distributed client applications.

See the official Rust SDK for client options.

Configuration and CLI

Both the Cargo helpers and the CLI read local inputs and generate the same Rust code. Use the CLI for explicit generation or drift checks. Use a Cargo helper to regenerate automatically when the consumer’s inputs change.

Rust configuration

Start with convex_typegen::config::Configuration::default() and use the setters to override fields. The struct is non-exhaustive. The setters accept paths convertible to PathBuf except with_function_paths, which takes a Vec<PathBuf>.

SettingDefault and effect
convex_dir: PathBuf
with_convex_dir
convex. Root for source discovery, module names, and the default schema path.
schema_path: Option<PathBuf>
with_schema_path
None. Use <convex_dir>/schema.ts. An explicit override is resolved independently of convex_dir.
function_paths: Vec<PathBuf>
with_function_paths
Empty. Discover functions under the Convex root. A non-empty list replaces discovery unless a spec is configured. Files outside the root do not produce public references.
function_spec_path: Option<PathBuf>
with_function_spec_path
None. When set, use this JSON file as the complete function inventory and ignore function_paths. The schema is still read from TypeScript.
out_file: PathBuf
with_out_file
$OUT_DIR/convex_api.rs if the environment variable is set; otherwise the relative path OUT_DIR/convex_api.rs. The CLI requires an explicit output when the variable is unset.

Relative paths resolve from the current working directory: the consumer package root in Cargo’s build script, and the directory from which you invoke the CLI. Only the implicit schema path is joined to convex_dir. For example, from apps/desktop, --convex-dir ../backend/convex --schema overrides/schema.ts reads apps/desktop/overrides/schema.ts and discovers modules under apps/backend/convex. config.resolved_schema_path() returns the selected schema path before filesystem resolution.

Cargo helpers and rerun tracking

  • convex_typegen::build::generate_out_dir() uses the default layout and requires Cargo’s OUT_DIR. It writes convex_api.rs there.
  • convex_typegen::build::generate(config) accepts a custom configuration. See the monorepo example.
  • Both helpers emit cargo:rerun-if-changed for the schema and loaded relative imports, plus the selected spec or TypeScript function files and their loaded imports. Discovery also watches the Convex directory for added or removed modules.
  • The crate-root convex_typegen::generate(config) writes bindings without Cargo directives. The crate-root convex_typegen::check(config) reports whether output is current without writing. The verbose Cargo feature prints additional parser and semantic diagnostics.

CLI

convex-typegen [--check] [--schema PATH] [--convex-dir PATH] [--output PATH]
               [--function-spec PATH] [--function PATH]
FlagBehavior
--convex-dir PATHSet the Convex root; defaults to convex.
--schema PATHOverride the schema; defaults to <convex-dir>/schema.ts.
--function PATHParse this explicit function source. Repeat for each source. With none, discover files.
--function-spec PATHRead an authoritative spec. Takes precedence over every --function value.
--output PATHSet the output file. Required unless OUT_DIR is set, in which case the default is $OUT_DIR/convex_api.rs.
--checkOff by default. Compare generated bytes with the existing output without modifying it.
--help, -hPrint usage and exit.

Repeated single-path flags use the last value. Pass flags and paths as separate arguments. Exit status 0 means success (including current output in check mode), 1 means check mode found missing or stale output, and 2 means an invalid invocation or generation error.

For example, to check a committed output against a spec in a neighboring package:

convex-typegen --check --convex-dir ../backend/convex \
  --function-spec ../backend/function-spec.json --output src/convex_api.rs

Support matrix

This page answers two practical questions: what convex-typegen can generate from your project today, and what to do when static analysis is not enough.

Rust versions

CrateMSRVWhen you need it
convex-typegen (generator)1.97Any project that runs generation in build.rs
convex-typegen-runtime1.85Every consumer; enough on its own when bindings are pre-generated

CI checks each crate at its declared MSRV.

What you can generate offline

The generator reads convex/schema.ts for tables and either:

  1. TypeScript under your Convex root — exported public queries, mutations, and actions whose validators are visible to the offline resolver, or
  2. A committed function-spec JSON file — the public function list and resolved validators from a deployment you captured with the Convex CLI.

Start with TypeScript. When a function builds validators at runtime, spreads from unknown sources, or uses patterns the resolver skips, generation fails with a file path and span when possible. That is your signal to generate and commit a function spec instead of guessing types.

Supported Convex validators

These match Convex 1.49-style validator types:

ValidatorGenerated shape (typical)
v.string(), v.number(), v.float64(), v.boolean(), v.null()String, f64, bool, () for null
v.int64(), v.bigint(), v.commitTs()i64 / Int64
v.bytes()value_serde::Bytes
v.id("table")model::Id<tables::Table>
v.literal(value)Exact literal model; use v.null() for null
v.optional(T), v.nullable(T)See generated models
v.array(T), v.object({…}), v.record(key, value)Vec, struct, BTreeMap with string-like keys
v.union(a, b, …)Enum or nested union models
v.any()model::RawValue
v.function(signature?)Function handle type

The schema parser accepts required object tables and unions of required object shapes, including .index(), .searchIndex(), and .vectorIndex() chains. Unsupported validator calls, dynamic record keys, or invalid record key types fail loudly during generation.

Raw values and undeclared validators

When an endpoint omits an args or return validator, codegen still emits a reference—you pass or receive RawArgs / RawValue instead of a typed struct. Declared empty args {} still get a dedicated empty struct.

Committing a function spec is the supported way to get typed args and returns for functions whose TypeScript cannot be analyzed fully offline.

TypeScript the offline parser understands

By default the tool scans .ts files under convex_dir, skipping the resolved schema file, _generated/, node_modules/, declaration files, http.ts, crons.ts, and convex.config.ts. You can replace discovery with an explicit file list in Configuration::function_paths.

Supported patterns include:

  • Exported const endpoints calling query, mutation, action, or their internal* variants imported from convex/server or /_generated/server.
  • Inline validators, same-file const aliases, and named validators imported from relative modules (including sibling constants in the imported file).
  • Static object spreads that resolve to object literals.
  • TypeScript as / satisfies wrappers around validators—they are peeled off.

Not supported: validators built at runtime, cyclic imports, most dynamic spreads, and unsupported export or import forms.

Function discovery and generated module names are based on files under the configured Convex root. Explicit function_paths do not make files outside that root eligible for generated API references.

Reference: resolver limits

To keep generation predictable, validator following and object-spread inlining stop at depth 64. Beyond that, or when an expression is not one of the supported forms above, use a committed function spec captured from a backend that already resolved the validators.

A function spec replaces TypeScript function discovery entirely but does not replace schema.ts—table definitions still come from your schema file.

What appears under api

Only public queries, mutations, and actions become api references. Internal functions, HTTP routes in http.ts, and crons in crons.ts are skipped. Folder layout mirrors Convex modules. See path-scoped names.

Rust API reference

For individual Rust items, signatures, and trait implementations, see rustdoc:

For tasks and complete examples, start with the first project or the configuration and CLI reference.

Troubleshooting

The build cannot find the schema

Check the path reported by the generator. In a workspace, relative paths start at the package containing build.rs. Set the Convex root or schema override.

A function is missing or its types are raw

Make sure the function is public and its module lives under the configured Convex root. Declare both args and returns; args: {} explicitly declares no arguments. Check the supported source patterns. If you configured a spec, inspect that file for the endpoint and its validators and refresh it if it is stale.

include! cannot find the output

Check that the build script ran in this same package and wrote the expected filename under its OUT_DIR. Use the filename form of the macro for alternate Cargo output filenames, or a normal module declaration for committed bindings.

Compilation succeeds but calls fail

Verify that the app’s URL points to the deployment containing these functions. Check the endpoint’s authentication requirements. A CallError::Decode { .. } can mean the deployed return validator and generated bindings are out of sync. See application errors to identify the failure.

A subscription times out

Keep the SDK client alive, poll the stream, and check that your mutation changes data read by the query. A subscription emits query results, so a write that leaves the result unchanged need not produce a new item. Use a unique write value in a smoke test.

Develop this project

The contributor guide covers building, testing, and proposing changes. Consumer chapters live under docs/site/src/; edit those Markdown sources and use just docs to preview the mdBook. The first project includes code directly from examples/readme/.

Local self-hosted Convex E2E

The v1 typed-client acceptance gate deploys the committed review fixture to the open-source Convex backend running in Docker on the same machine. It never uses a Convex cloud deployment. Backend image version 24.04 is pinned by digest, both exposed ports bind to loopback, and the runner creates a fresh admin key inside the container for each run.

From the workspace root, run:

just e2e
just e2e tutorial

The second command deploys and type-checks the first-project tutorial on a fresh local backend, then runs the Rust example with offline Cargo.

Prerequisites are Docker, Node.js/npm, jq, current stable Rust, network access to the locked package registries, and just. The script fetches locked Rust dependencies first, then runs the consumer build offline. The local backend uses 127.0.0.1:13210 (API) and 127.0.0.1:13211 (HTTP actions); keep those ports free. The command installs the fixture’s locked npm dependencies, deploys it with the self-hosted CLI, captures a fresh function spec, then runs the Rust consumer with Cargo offline. Its build script feeds the captured spec to the generator and includes the generated bindings from OUT_DIR.

The executable checks nested typed queries, a typed mutation and returned Id, Doc results, a successful action, and the decoded ConvexError payload. It also checks root and nested function paths, omitted internal/HTTP/cron functions, table-union serialization, omitted optional fields versus required null fields, record values, exact Int64/Float64/Bytes echo, and the initial and subsequent typed subscription results. Every backend operation and subscription wait has a 20-second timeout; backend startup is bounded to 60 retries.

The runner always writes sanitized Docker output to target/e2e-artifacts/backend.log (or E2E_ARTIFACT_DIR/backend.log when set) and removes the backend container. CI compiles convex-typegen-e2e with the committed function spec. Run backend tests on your local machine with just e2e.

E2E fixture project

The checked-in consumer and fixture files used by the local self-hosted verification flow live in the repository’s e2e/ directory. Read the backend setup and test coverage.

Semantic versioning policy

From 1.0.0 onward, convex-typegen follows SemVer 2.0 on the crate version. Earlier 0.x releases shipped breaking changes in minor versions; that history lives in the CHANGELOG.

Two surfaces, two semver stories

SurfaceWhat it isSemver
Library APIRust items you import from convex_typegen (generate, Configuration, errors, prelude, features) or convex_typegen_runtime (client helpers, codecs, generated-code support)Standard semver on the crate version
Generated outputconvex_api.rs (or your out_file) emitted at build timeNot the published Rust API, but intentional shape changes are breaking for your app and bump the crate major

Use matching release versions of convex-typegen and convex-typegen-runtime. Put the generator in [build-dependencies] and the runtime in [dependencies]. Regenerate bindings whenever you upgrade so generated code and runtime stay in sync.

Library API

These items are semver-stable from 1.0.0:

  • convex_typegen::{generate, check}
  • convex_typegen::config::Configuration, its fields, and its setters
  • convex_typegen::build::{generate, generate_out_dir}
  • convex_typegen::ConvexTypeGeneratorError
  • convex_typegen::prelude re-exports
  • Generator Cargo feature verbose
  • Runtime TypedClient, CallError, TransportError, InvalidApplicationData, function, model, value_serde, and prelude
  • Runtime macros ConvexValue and include!, and crate-root convex, serde, and serde_json re-exports

The runtime wraps the official SDK directly. An incompatible update of a publicly exposed convex 0.x type (for example, ConvexClient or Value) requires a runtime major release.

See api-stability.md for a per-item table.

Generated output (codegen semver)

Treat any intentional change to generated Rust that breaks consumer code as requiring a major crate bump:

  • Renaming or removing pub struct / pub enum types
  • Changing field types on generated structs
  • Changing TryFrom / Serialize behavior for args structs
  • Renaming or removing generated api references

Non-breaking codegen (minor crate bump):

  • New generated types for newly added Convex functions or tables only (existing names unchanged)
  • Additional #[allow(...)] or comments in the generated header
  • Bug fixes that make previously uncompilable output compile without renaming existing types

When in doubt, bump major.

Version bump rules

ChangeBump
New variant on a non-exhaustive public error enumMinor
New optional field on Configuration with a defaultMinor
New Cargo feature, default offMinor
New Cargo feature, default onMajor (or avoid; use default off)
Remove/rename Configuration field or generate signature changeMajor
MSRV increase (rust-version in Cargo.toml)Major
Generated type rename or field type change for existing Convex exportsMajor
Dependency-only update with no API/codegen changePatch or minor (note in CHANGELOG)

MSRV policy

  • The generator requires Rust 1.97. The runtime and derive crate require Rust 1.85, matching the official Convex client. A project that runs the generator in build.rs requires Rust 1.97. Pregenerated bindings can use the matching runtime on Rust 1.85.
  • Raising MSRV is a major version change.
  • CI verifies each crate at its declared MSRV.

Maintainer releases

Default branch is master.

  1. Update CHANGELOG.md and workspace.package.version in Cargo.toml.
  2. Add docs/release-vX.Y.Z.md, merge to master, tag vX.Y.Z, and create a GitHub Release.
  3. Publish to crates.io with cargo publish --locked (or just publish). Publish the macro crate before the runtime and keep its exact version requirement in runtime/Cargo.toml in sync with the workspace version. There is no automated publish workflow.

Contributors: add user-visible changes under [Unreleased] in CHANGELOG.md.

API stability

Quick reference for what is stable vs unstable in convex-typegen. Full rules: semver-policy.md.

Stable (1.0+)

ItemStabilityNotes
generate(config)StableWrite the configured artifact
check(config)StableCheck for drift without writing
config::ConfigurationStableNon-exhaustive. Construct with Default and chainable setters
build::generate(config)StableOffline Cargo generation with input tracking
build::generate_out_dir()StableStandard Convex layout and $OUT_DIR/convex_api.rs
ConvexTypeGeneratorErrorStableCrate-root error export with a non-exhaustive variant set and fields
prelude::*StableSee below
Feature verboseStableDefault off

The generator prelude exports Configuration, generate, check, and ConvexTypeGeneratorError.

Runtime crate

ExportRole
ConvexValue, include!Derive for Convex wire models and helper for including generated bindings
convexRe-export of the SDK used by the runtime
serde, serde_jsonRe-exports used by generated bindings
TypedClient, CallError, TransportError, InvalidApplicationDataTyped calls, subscriptions, and errors over the official SDK
functionTyped endpoint references, kind markers, raw args, and the sealed FunctionReference trait
model::{Doc, Id, RawValue, Table}Document models, typed IDs, raw wire values, and table markers
value_serde::{to_value, from_value, Bytes, Error}Direct Serde conversion to and from convex::Value
prelude::*Serde derives, typed client, codecs, and ConvexJsonValue, ConvexBytes, and ConvexValueSerdeError aliases

Runtime errors are non-exhaustive. Match CallError with a fallback and include .. when matching any variant or destructuring InvalidApplicationData. The runtime exposes official SDK types directly. An incompatible update to a publicly exposed convex 0.x type requires a runtime major release.

Unstable / not semver-guaranteed

ItemNotes
Generated convex_api.rsType names and fields can change on major crate bumps
Internal modulesconvex_typegen::convex is private; do not depend on it
Exact error Display stringsMay improve in patches; match on variants, not text
Dependency versionsPrivate dependency updates can affect build time. Public SDK type and MSRV changes follow the semver policy.

Generated code contract

Generated files include a header stating they are regenerated on each build. Do not edit by hand. Stability of generated names and types follows codegen semver in semver-policy.md, not the library API table above.