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.
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::listtoquery, notmutation, 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
returnsvalidators become Rust result types (for exampleVec<String>fortasks::list,Id<tables::Tasks>fortasks::add). - Tables and documents —
tables::*models,Id<T>, and document fields line up withconvex/schema.ts. - Optionals vs null —
v.optionaland nullable unions encode as distinct Rust types where the generator models them. - Application errors —
with_error::<YourPayload>()ties a declared error shape to decoding viaCallError.
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()onTypedClientreturns&mut convex::ConvexClientfor auth,watch_all, and untypedquery/mutation/actionusingpath()plus raw Convex values.- Re-exported
convexcrate —convex_typegen_runtime::convexis 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
Idstill 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.
- Describe the wire contract. Write the table schema and declare each public function’s
argsandreturns. The generator reads the schema and either the function sources or a committed function spec. - Generate at build time. The
convex-typegenbuild dependency writes Rust code. The default Cargo helper writes toOUT_DIRand registers inputs so Cargo knows when to rerun it. - Include and use the types. The
convex-typegen-runtimeapplication dependency providesinclude!(), the wire codec, andTypedClient. The client adapts the official Convex SDK. Addconvexas 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
| Operation | Use it when | Tutorial example |
|---|---|---|
query | You need one result from a read. | client.query(tasks::list, tasks::ListArgs {}).await? |
mutation | You need a transactional write and its result. | client.mutation(tasks::add, args).await? |
action | You 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 capability | Where to use it | Example |
|---|---|---|
Read once with query | Typed method | client.query(tasks::list, tasks::ListArgs {}).await? |
Write with mutation | Typed method | client.mutation(tasks::add, args).await? |
Run an action | Typed method | client.action(tasks::ping, tasks::PingArgs {}).await? |
Follow one query with subscribe | Typed method | client.subscribe(tasks::list, tasks::ListArgs {}).await? |
| Set or clear a user token | client_mut() | client.client_mut().set_auth(Some(token)).await; Pass None to sign out. |
Fetch fresh tokens with set_auth_callback | client_mut() | client.client_mut().set_auth_callback(Some(fetch_token)).await; See the callback type below. |
| Authenticate as a deployment admin | client_mut() | client.client_mut().set_admin_auth(admin_key, None).await; |
Watch all active queries with watch_all | client_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 calls | client_mut() | client.client_mut().query(tasks::list.path(), raw_args).await? See how to encode raw_args below. |
| Connect with default options | Before wrapping | let mut sdk = convex::ConvexClient::new(url).await?; |
| Configure client ID or connection-state notifications | Before wrapping | convex::ConvexClientBuilder::new(url).with_client_id("tasks").with_on_state_change(state_tx) |
| Connect with a builder | Before wrapping | let mut sdk = convex::ConvexClient::new_from_builder(builder).await?; You can also call builder.build().await?. |
| Share a connection across SDK handles | Before wrapping | let 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
| Failure | What 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".Noneon a required nullable field sends JSONnull.
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 JSONnull.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:
| Intent | Rust value | Convex object |
|---|---|---|
| Leave the field unspecified | None | No priority key |
| Send an explicit empty priority | Some(None) | { priority: null } |
| Assign high priority | Some(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:
| Situation | Generated type | What to do |
|---|---|---|
| No args validator | function::RawArgs (BTreeMap<String, RawValue>) | Build a map or use RawArgs::new() for {} |
| Empty args object validator | Empty endpoint-specific struct | Pass MyArgs {} |
| No return validator | model::RawValue | Decode manually or narrow with from_value |
v.any() | model::RawValue | Same 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)]onOption<T>to omitNoneand decode a present key asT. An unmarkedOption<T>is required and nullable. An optional field can also accept null when its inner type accepts null, such as the innerOption<Priority>above. - Use
i64for Int64,f64for Float64,Bytesfor bytes, andBTreeMap<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. Write1or1_i64for Int64 and1.0for 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 requiredkindfield. 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>.
| Setting | Default and effect |
|---|---|
convex_dir: PathBufwith_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: PathBufwith_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’sOUT_DIR. It writesconvex_api.rsthere.convex_typegen::build::generate(config)accepts a custom configuration. See the monorepo example.- Both helpers emit
cargo:rerun-if-changedfor 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-rootconvex_typegen::check(config)reports whether output is current without writing. TheverboseCargo feature prints additional parser and semantic diagnostics.
CLI
convex-typegen [--check] [--schema PATH] [--convex-dir PATH] [--output PATH]
[--function-spec PATH] [--function PATH]
| Flag | Behavior |
|---|---|
--convex-dir PATH | Set the Convex root; defaults to convex. |
--schema PATH | Override the schema; defaults to <convex-dir>/schema.ts. |
--function PATH | Parse this explicit function source. Repeat for each source. With none, discover files. |
--function-spec PATH | Read an authoritative spec. Takes precedence over every --function value. |
--output PATH | Set the output file. Required unless OUT_DIR is set, in which case the default is $OUT_DIR/convex_api.rs. |
--check | Off by default. Compare generated bytes with the existing output without modifying it. |
--help, -h | Print 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
| Crate | MSRV | When you need it |
|---|---|---|
convex-typegen (generator) | 1.97 | Any project that runs generation in build.rs |
convex-typegen-runtime | 1.85 | Every 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:
- TypeScript under your Convex root — exported public queries, mutations, and actions whose validators are visible to the offline resolver, or
- 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:
| Validator | Generated 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
constendpoints callingquery,mutation,action, or theirinternal*variants imported fromconvex/serveror/_generated/server. - Inline validators, same-file
constaliases, and named validators imported from relative modules (including sibling constants in the imported file). - Static object spreads that resolve to object literals.
- TypeScript
as/satisfieswrappers 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:
convex-typegen— generator, build helpers, and configuration.convex-typegen-runtime— typed client, models, macros, and codecs.convex— official Convex Rust SDK.
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 end-to-end checks explain how to exercise the Rust client against a disposable, self-hosted Convex backend. CI compiles the examples without starting Convex.
- Architecture maps the pipeline and its invariants for maintainers.
- Semantic versioning and API stability describe how to review changes to the public contract.
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
| Surface | What it is | Semver |
|---|---|---|
| Library API | Rust 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 output | convex_api.rs (or your out_file) emitted at build time | Not 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 settersconvex_typegen::build::{generate, generate_out_dir}convex_typegen::ConvexTypeGeneratorErrorconvex_typegen::preludere-exports- Generator Cargo feature
verbose - Runtime
TypedClient,CallError,TransportError,InvalidApplicationData,function,model,value_serde, andprelude - Runtime macros
ConvexValueandinclude!, and crate-rootconvex,serde, andserde_jsonre-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 enumtypes - Changing field types on generated structs
- Changing
TryFrom/Serializebehavior for args structs - Renaming or removing generated
apireferences
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
| Change | Bump |
|---|---|
| New variant on a non-exhaustive public error enum | Minor |
New optional field on Configuration with a default | Minor |
| New Cargo feature, default off | Minor |
| New Cargo feature, default on | Major (or avoid; use default off) |
Remove/rename Configuration field or generate signature change | Major |
MSRV increase (rust-version in Cargo.toml) | Major |
| Generated type rename or field type change for existing Convex exports | Major |
| Dependency-only update with no API/codegen change | Patch 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.rsrequires 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.
- Update
CHANGELOG.mdandworkspace.package.versioninCargo.toml. - Add
docs/release-vX.Y.Z.md, merge tomaster, tagvX.Y.Z, and create a GitHub Release. - Publish to crates.io with
cargo publish --locked(orjust publish). Publish the macro crate before the runtime and keep its exact version requirement inruntime/Cargo.tomlin 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+)
| Item | Stability | Notes |
|---|---|---|
generate(config) | Stable | Write the configured artifact |
check(config) | Stable | Check for drift without writing |
config::Configuration | Stable | Non-exhaustive. Construct with Default and chainable setters |
build::generate(config) | Stable | Offline Cargo generation with input tracking |
build::generate_out_dir() | Stable | Standard Convex layout and $OUT_DIR/convex_api.rs |
ConvexTypeGeneratorError | Stable | Crate-root error export with a non-exhaustive variant set and fields |
prelude::* | Stable | See below |
Feature verbose | Stable | Default off |
The generator prelude exports Configuration, generate, check, and ConvexTypeGeneratorError.
Runtime crate
| Export | Role |
|---|---|
ConvexValue, include! | Derive for Convex wire models and helper for including generated bindings |
convex | Re-export of the SDK used by the runtime |
serde, serde_json | Re-exports used by generated bindings |
TypedClient, CallError, TransportError, InvalidApplicationData | Typed calls, subscriptions, and errors over the official SDK |
function | Typed 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
| Item | Notes |
|---|---|
Generated convex_api.rs | Type names and fields can change on major crate bumps |
| Internal modules | convex_typegen::convex is private; do not depend on it |
Exact error Display strings | May improve in patches; match on variants, not text |
| Dependency versions | Private 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.