Convex Schema Parser & Client Generator
convex-schema-parser
is a simple command-line tool designed to parse your Convex project's schema and function definitions, generating strongly-typed API clients for both Rust and Python.
It offers two primary modes of operation:
-
A one-shot generate
command for manual client generation.
-
A persistent dev
mode that watches your Convex project for changes and automatically regenerates your clients, providing a seamless development experience.
[!IMPORTANT]
At the bottom you will find a USAGE section
Installation
The easiest way to use convex-schema-parser
is currently through the Cabal package manager.
[!NOTE]
We are on hackage now! So you can simply run cabal update
(important, this package is a recent addition to Hackage) and then cabal install convex-schema-parser
to install the tool quickly without cloning.
We provide prebuilt binaries for linux
& macOS
that you can download and run directly, but macOS
users have to allow the binary to run first since we do not sign it (yet).
A npm
package @parsonosai/convex-schema-parser
is also on its way and supported as soon as we get code-signing ready, we currently use a placeholder.
Installing cabal
& ghc
is best done using ghcup
. As soon as it is installed:
ghcup install ghc 9.10.1
ghcup install cabal 3.10.3.0
You can then build/run the tool from source.
cabal update
cabal install convex-schema-parser # If you have $HOME/.cabal/bin in your PATH.
cabal run convex-schema-parser -- --help # If you do not want to install it globally and just run it.
Prerequisites
[!NOTE]
Everything here is also explained when you issue conves-schema-parser init
.
Before using the tool, please ensure your environment meets the following requirements:
1. pnpm
or npm
Installation
The tool shells out to your package manager to generate TypeScript declaration files (.d.ts
). You must have pnpm
or npm
installed and available in your system's PATH.
2. package.json
Script
Your Convex project's package.json
must contain a script named declarations
. This script is responsible for running all the necessary steps to generate the .d.ts
files for the parser to read. This often involves cleaning old artifacts, running the TypeScript compiler, and copying over pre-generated files.
Everything here will also be explained when you issue convex-schema-parser init
, this command will also create a template convex-parser.yaml
file.
Example package.json
:
{
"scripts": {
"declarations:clean": "rm -rf tmp",
"declarations:build": "tsc -p tsconfig.declarations.json",
"declarations:copy-generated": "cp -r convex/_generated tmp/declarations/",
"declarations": "npm run declarations:clean && npm run declarations:build && npm run declarations:copy-generated",
"test": "echo \"Error: no test specified\" && exit 1"
}
}
3. tsconfig.declarations.json
The tool assumes a specific tsconfig.json
file exists to guide the declaration generation process. This file should be configured to only emit declaration files into a designated output directory (e.g., tmp/declarations
) from your source convex/
directory.
Example tsconfig.declarations.json
:
{
"compilerOptions": {
// --- Output Configuration ---
"outDir": "./tmp/declarations", // Output all files to a temporary directory
"rootDir": "./convex", // The root of the source files we care about
// --- Generation Settings ---
"declaration": true, // Generate .d.ts files
"emitDeclarationOnly": true, // Don't generate any .js files
"skipLibCheck": true, // Speeds up compilation by not checking library files
// --- Module Settings (to match Convex) ---
"module": "commonjs",
"target": "es2020"
},
// Tell tsc where to find the files to compile
"include": ["convex/**/*.ts"]
}
Usage
The tool is run from the command line and has two main subcommands: generate and dev.
generate
Command (One-Shot)
This command parses your project once, generates the specified client, and then exits. It's useful for CI/CD pipelines or manual updates.
convex-schema-parser generate --schema <path> --declarations <path> --target <lang> [-o <output_file>]
# Or if you are not using the installed binary but via cabal (same for the other commands):
cabal run convex-schema-parser -- generate --schema <path> --declarations <path> --target <lang> [-o <output_file>]
Arguments:
-
--schema
: (Required) Path to your main schema.ts
file.
-
--declarations
: (Required) Path to the root directory where tsc
generated the .d.ts
files (the outDir
from your tsconfig
).
-
--target
: (Required) The target language. Can be Python
or Rust
.
-
-o, --output
: (Optional) The file to write the generated code to. If omitted, the code will be printed to standard output.
Example:
cabal run convex-schema-parser -- generate \
--schema /path/to/my-project/convex/schema.ts \
--declarations /path/to/my-project/tmp/declarations \
--target Rust \
--output /path/to/my-rust-app/src/convex_api.rs
dev
Command (Watch Mode)
This is the recommended mode for local development. It starts a persistent process that watches your convex/
directory for any file changes. When a change is detected, it automatically runs the pnpm declarations
(or npm run declarations
) script and regenerates all clients defined in your configuration file.
convex-schema-parser dev [--config <path>]
Arguments:
--config
: (Optional) The path to your YAML configuration file. Defaults to convex-parser.yaml
in the current working directory.
Configuration (convex-parser.yaml
)
The dev
mode is driven by a YAML configuration file. This file allows you to define multiple generation targets, enabling you to generate clients for different languages and output them to multiple locations simultaneously.
Example convex-parser.yaml
:
# Configuration for the Convex Client Generator.
# (Required) The absolute path to the root of your Convex project.
# This is the directory that contains the `convex/` folder and `package.json`.
project_path: /path/to/your/convex/project
# (Required) The absolute path to the generated TypeScript declarations, relative to `project_path`.
declarations_dir: /path/to/your/tmp/declarations
# (Optional) The path where validation sandbox projects will be created.
# Defaults to `~/.config/convex-schema-parser` if omitted.
validation_path: "~/.config/convex-schema-parser"
# (Required) A list of generation targets. You can have one or more.
targets:
# Example 1: Generate a Rust client for a backend service.
- lang: Rust
# A list of one or more output files for this target.
output:
- ../my-rust-app/src/convex_api.rs
- ../my-other-app/src/convex_api.rs
# Example 2: Generate a Python client for data scripts.
- lang: Python
output:
- ../scripts/lib/convex_client.py
Configuration Schema:
project_path
: The absolute path to your Convex project root directory. This directory should contain the convex/
folder and a package.json
file.
declarations_dir
: The absolute path to the directory where your TypeScript declaration files (.d.ts
) are generated. This should be the output directory specified in your tsconfig.declarations.json
.
validation_path
: (Optional) The path where validation sandbox projects will be created. If omitted, defaults to ~/.config/convex-schema-parser
.
targets
: A opt-level key holding a list []
of target configurations.
lang
: The target language for the client. Must be Python
or Rust
.
output
: A list []
of file paths where the generated client code will be written. Each target can have multiple output paths.
API Usage Examples
Once you have generated your client code, you can use it in your projects.
Python Client Example
The generated Python client uses nested classes to mirror your Convex project's file structure.
Queries, Mutations and Actions
import os
from convex import ConvexClient
# Import the generated API module (e.g., convex_api.py)
import convex_api
# 1. Instantiate the official ConvexClient with your deployment URL.
deployment_url = os.environ.get("CONVEX_URL")
client = ConvexClient(deployment_url)
# 2. Instantiate your generated API, wrapping the client.
auth_key = get_auth_key() # Replace with your method to get the auth/api/jwt key if required
client.set_auth(auth_key)
api = convex_api.API(client)
# 3. Call functions using the nested structure.
# This corresponds to the function `getProject` in `convex/functions/projects.ts`.
# The generated API reraises any exceptions from the Convex client, so you can handle them as needed.
# Additionally, we use `pydantic` for type validation, so we raise these exceptions as well.
try:
project_id = convex_api.Id("prj_...")
project = api.functions.projects.get_project(project_id)
if project:
print(f"Successfully fetched project: {project.project_name}")
else:
print("Project not found.")
except Exception as e:
print(f"An error occurred: {e}")
Subscriptions
from convex import ConvexError
# ... (assuming `api` is already instantiated and authenticated)
try:
# 1. Call the generated `subscribe_*` method. This returns a generator instantly.
tenant_id = convex_api.Id("tnt_...")
project_subscription = api.functions.queries.subscribe_fetch_projects(tenant_id)
print("Subscribed to projects. Waiting for updates... (Press Ctrl+C to stop)")
# 2. The `for` loop starts the subscription and blocks until the first value
# is received. The loop body will run again for each subsequent update.
for updated_projects in project_subscription:
# 3. `updated_projects` is already a fully validated Pydantic model
# (e.g., list[FetchProjectsReturnObject]).
print(f"Received update with {len(updated_projects)} projects:")
for project in updated_projects:
print(f" - ID: {project._id}, Name: {project.project_name}")
except ConvexError as e:
print(f"Subscription failed with an error: {e}")
except KeyboardInterrupt:
print("\nSubscription stopped by user.")
Rust Client Example
The generated Rust client uses a method-based API which works with Rust's ownership and borrowing rules.
Queries, Mutations and Actions
// Assuming the generated module is named `convex_api`.
use convex_api::{Api, Id, types::ProjectsDoc};
use convex::ConvexClient;
use anyhow::Result;
#[tokio::main]
async fn main() -> anyhow::Result<()> {
// 1. Instantiate and authenticate the official ConvexClient.
let convex_url = std::env::var("CONVEX_URL")?;
let auth_key = get_auth_key(); // Replace with your method to get the auth/api/jwt key if required
let mut convex_client = ConvexClient::new(&convex_url).await?;
convex_client.set_auth(&auth_key);
// 2. Instantiate your generated API, wrapping the client.
let mut api = Api::new(convex_client);
// 3. Call functions using the nested, method-chaining API.
let project_id = Id::<ProjectsDoc>::new("prj_...".to_string());
// This corresponds to the function `getProject` in `convex/functions/projects.ts`.
match api.functions().projects().get_project(&project_id).await {
Ok(Some(project)) => {
println!("Successfully fetched project: {}", project.project_name.unwrap_or_default());
}
Ok(None) => {
println!("Project not found.");
}
Err(e) => {
eprintln!("An error occurred: {}", e);
}
}
Ok(())
}
Subscriptions
use futures_util::stream::StreamExt;
// ... (assuming `api` is already instantiated and authenticated)
async fn run_subscription() -> anyhow::Result<()> {
// 1. Call the generated `subscribe_*` method.
let tenant_id = convex_api::Id::<convex_api::types::TenantsDoc>::new("tnt_...".to_string());
let mut project_subscription = api.functions().queries().subscribe_fetch_projects(&tenant_id).await?;
println!("Subscribed to projects. Waiting for updates... (Press Ctrl+C to stop)");
// 2. The `while let` loop asynchronously polls the stream for new items.
while let Some(result) = project_subscription.next().await {
// 3. Each `result` is a `Result<T, ApiError>`, where T is your strongly-typed
// return value (e.g., Vec<FetchProjectsReturnObject>).
match result {
Ok(updated_projects) => {
println!("Received update with {} projects:", updated_projects.len());
for project in updated_projects {
println!(" - ID: {}, Name: {}", project._id, project.project_name.unwrap_or_default());
}
}
Err(e) => {
eprintln!("Received an error in the subscription stream: {}", e);
}
}
}
Ok(())
}