You send the Rust source as text and nothing else. Substrio compiles it to WebAssembly and then to native code when you deploy it, so no toolchain is needed where you write it. A Rust function is routed, gated, limited, counted, and logged exactly as a JavaScript function is, and it receives the same request and gives back the same kind of response. It is the second function language of an app. A function keeps its name and its routes when you move it between JavaScript and Rust.
When to pick Rust
- Pick Rust for computational work such as parsing, encoding, hashing, scoring, or processing images and text.
- Pick JavaScript for a function that mostly calls the platform, because the time then goes to those calls.
- A Rust function is one source file. It can use the
substrio_wasmcrate,core, andalloc, and no other crate. The crate is provided by the server, so there is nothing to install. - A request body over 1 MiB is refused with
REQUEST_TOO_LARGEbefore the module runs. JavaScript functions stream larger bodies. - A Rust function that needs a service the crate has no call for can run any operation of the registry with
substrio_wasm::api.
A minimal function
#![no_std]
extern crate alloc;
use alloc::format;
use substrio_wasm::{Ctx, Env, Request, Response};
fn fetch(req: &Request, env: &Env, _ctx: &Ctx) -> Response {
let name = req.param("name").unwrap_or("world");
let greeting = env.get("GREETING").unwrap_or("Hello");
substrio_wasm::console::log("hello called");
Response::text(200, &format!("{greeting}, {name}!"))
}
substrio_wasm::export!(fetch);
fetch(request, env, ctx) has the shape of the JavaScript entry. export! writes the three exports a module needs, and a source without it does not link. The other entries (a cron run, a queue delivery, a document trigger, and a direct call) are named in export! as well, and wasm-functions.md lists them.
Every call exposed to Rust
Every call is a function or method of the substrio_wasm crate. Each has a JavaScript equivalent with the same behavior and refusal reasons. The linked pages provide signatures, results, errors, and examples.
| Area | Crate calls | What they do | Page |
|---|---|---|---|
| The request | Request::method, url, path, query, headers, header, header_bytes, params, param, route, body, text, signed_in, auth_json, caller_json, ip, tenant |
Read the call in place. Nothing is parsed or copied. | wasm-functions.md |
| The response | Response::new, text, json, body, body_mut, header, content_type, status, with_status |
Build the answer. | wasm-functions.md |
| Settings and operations | Env::get, Env::pairs, substrio_wasm::api, substrio_wasm::api_as |
Read the app's secrets and the function's vars, and run any operation of the registry as the app or as the caller. | wasm-functions.md |
| The caller | auth::current_user, auth::check_permission |
Read the caller's user and test one of its permissions. | wasm-functions.md |
| Logs | console::log, log! |
Write one line in the function's log. | wasm-functions.md |
| Other entries | export! with scheduled, queue, trigger, call, and Batch, Message, Outcome (ack, retry, retry_after, ack_all, retry_all) |
Answer a cron run, a queue delivery, a document trigger, or a direct call. | wasm-functions.md |
| Collections | db::collection, then find_by_id, get, count, exists, insert, set, delete, field, and the chain methods eq, gte, sort, skip, limit, all, first, page, sum, avg, min, max |
Read and write documents. A read is made in place from the stored bytes. | wasm-functions.md |
| Records | update, update_row, update_many, delete_many, replace, insert_many, upsert_many, put, search, knn, has_many, belongs_to, link, unlink, and the declarations unique, expire, compound |
Write by id and by filter, search, relate, and declare how a collection is shaped. | wasm-records.md |
| KV | db::kv, then get, put, delete, has, increment, cursor, and the page calls |
Read and write the app's key-value stores in key order. | wasm-functions.md |
| SQL | db::sql, db::prepare, db::transaction, db::last_error_message |
Run one statement, keep one for many runs, and group statements in a transaction. | wasm-functions.md |
| The graph | db::graph, db::graph_labels, then add_edge, remove_edge, has_edge, neighbors, degree, edge_count |
Read and write the app's graphs. | wasm-functions.md |
| The ledger | db::ledger, then post, balance, top_balances, accounts, totals, clear, void, reverse, entries, and more |
Post entries and read balances. | wasm-ledger.md |
| Files | files::put, stat, get, get_into, remove, response, slice, fetch |
Store, read, and serve files. | wasm-ledger.md |
| Services | services::call, route, queue, search, collections, collection_pipeline, collection_stats, collection_drop, smtp, limit, once, secret, unique_space |
Call another function, test a route gate, send queue messages, search, send mail, limit a rate, and guard a write. | wasm-services.md |
| Permissions | services::permission, require_permission, holds_permission, holds_permissions, held_permissions, require_person_or_permission, require_verified |
Test and require the caller's declared permissions. | wasm-services.md |
| Objects, fetch, timers, crypto, clock | platform::objects, export_object!, fetch, wait_all, set_timeout, set_interval, sleep, random_uuid, get_random_values, date_now, performance_now |
Call a durable object, fetch a URL, set timers, read random bytes, and read the clock. | wasm-platform.md |
Each family has more calls than this table names. The five wasm- pages hold the complete lists.
Deploy
A deploy sends the source in rust where a JavaScript function sends script. Send exactly one of the two.
Over the API or MCP, function.deploy (POST /v1/deploy, MCP tool substrio_function_deploy) creates or replaces the function:
{ "name": "hello",
"rust": "#![no_std]\nextern crate alloc;\nuse alloc::format;\nuse substrio_wasm::{Ctx, Env, Request, Response};\n\nfn fetch(req: &Request, env: &Env, _ctx: &Ctx) -> Response {\n let name = req.param(\"name\").unwrap_or(\"world\");\n Response::text(200, &format!(\"Hello, {name}!\"))\n}\n\nsubstrio_wasm::export!(fetch);\n",
"routes": ["GET /hello/:name"] }
The response is {success, function_id, metadata}, and metadata.kind is "wasm". function.update (PATCH /v1/functions/{name}, tool substrio_function_update) takes the same body for an existing function.
With the CLI, put the source in functions/hello.rs, and put its routes in functions/hello.json beside it. Then run:
substrio deploy hello
substrio deploy hello compiles and replaces only that function. substrio deploy with no name deploys every .js and .rs file in functions/. --dry-run sends nothing, so it does not compile. A name that has both hello.js and hello.rs is refused with INVALID_ARGUMENT, because a function has one source.
The sidecar file functions/hello.json holds routes and tools only. Vars and the other settings of a function come from the [functions.hello] table of substrio.toml, from the vars of the API body, or from function.environment.set.
Compilation runs at deploy in two steps. rustc compiles the source with the crate into WebAssembly, within 120 s of time and CPU and 4 GiB of memory. Then substrio_wasmc checks the module against the ABI and translates it to native code. A source already compiled on a node is retrieved from a cache by its SHA-256. A refused deploy stores nothing, and the function continues to serve its previous source.
Testing a Rust function
These steps run against your own app with your own key, from your CLI profile (substrio login) or from your MCP connection. Each step names the CLI word, the operation, and the MCP tool. The CLI reaches any operation with substrio op NAME --data '{...}'. The steps are run as written by tests/integration/rust_walkthrough.test.mjs.
1. Compile it and read the diagnostics
function.check (POST /v1/_functions/_check, tool substrio_function_check, and substrio op function.check --data '{...}') takes the same body as a deploy. For a rust source it runs the compile that a deploy runs and stores nothing. A source that compiles answers { "ok": true, "format": "rust", "diagnostics": [] }. A source that does not compile answers ok: false with one diagnostic whose message starts with RUST_COMPILE_FAILED: and holds rustc's own text, as printed:
{ "ok": false, "format": "rust",
"diagnostics": [{ "severity": "error", "file": "hello", "line": 0, "column": 0,
"message": "RUST_COMPILE_FAILED: error[E0308]: mismatched types\n --> src/lib.rs:4:18\n ..." }] }
Each rustc diagnostic names src/lib.rs and a line and column of your source. Fix that line and check again. A deploy of the same source is refused with the status 422 and the code RUST_COMPILE_FAILED, and detail holds the same text:
{ "error": "The Rust source did not compile; the detail is the compiler's own diagnostics, ...",
"code": "RUST_COMPILE_FAILED", "status": 422, "retryable": false, "next_action": "correct_request",
"detail": "error[E0308]: mismatched types\n --> src/lib.rs:4:18\n ..." }
A deploy compiles the source too, so a source that passes the check is already compiled when you deploy it. For a function that exists, function.source.preview compiles a candidate edit and refuses it with the same code, and it stores nothing.
The other refusals at deploy are WASM_MODULE_INVALID (400, the module imports something Substrio does not provide), WASM_COMPILE_FAILED (422, the native compile did not finish within the deploy's limits), and RUST_TOOLCHAIN_UNAVAILABLE (503, the server has no Rust toolchain). The table is in wasm-functions.md.
2. Confirm what was deployed
substrio functions get hello
The answer holds the source in script and the function's metadata: kind is "wasm", and the routes and the content_hash are there as well. The same answer comes from function.get (GET /v1/functions/{name}) and the tool substrio_function_get. function.list shows every function of the app.
3. Call its routes
Call the route as a client would, with the URL of the app:
curl https://APP.DOMAIN/hello/ada
An agent connected over MCP can run the request through the platform with the tool substrio_route_call. It returns the status, the headers, and the body, and it takes a sandbox name to call a sandbox of the app instead. The tool has no HTTP route. Over HTTP, POST /v1/_route is refused with UNSUPPORTED_OPERATION, so send the request to the route on the app's host, or on <app>--<sandbox> for a sandbox.
4. Invoke it directly
function.invoke (CLI substrio invoke, tool substrio_function_invoke) calls a function by name with a JSON event. A Rust function answers it with its call entry, or with fetch when it has no call entry. The request body is the event.
substrio invoke hello --data '{"name":"ada"}'
5. Read its logs, errors, and refusals
A line written with console::log or log! is in the function's log:
substrio logs --function hello --since 5m
The same lines come from function.logs (GET /v1/_logs?function=hello, tool substrio_function_logs). --request-id ID shows every line of one call, and --follow --for 30s streams them live.
A call that ends in a trap (a panic, unreachable, a memory access out of bounds, or a stack overflow) answers 500 CF_EXECUTION_FAILED, and a panic's message is the error's message. A call that goes past a limit answers the status and code of the JavaScript limit: 408 CF_CPU_TIMEOUT, 413 CF_MEMORY_EXCEEDED, or 504 CF_WALL_TIMEOUT. The limits table is in wasm-functions.md, and errors-and-logs.md says how to read a failure. function.errors (tool substrio_function_errors) lists the errors the app's functions recorded, newest first.
A data call that is refused answers Err(db::Error { code }). Read the text of the last refusal with db::last_error_message(), and log it.
6. Edit and test again
substrio functions source hello
function.source.read reads the source in line ranges: GET /v1/functions/NAME/source?start_line=1&end_line=40 reads lines 1 to 39, because end_line is exclusive and both are required. A range is changed with function.source.preview and then function.source.edit, and the whole source with function.source.replace (rust in the body). Each compiles the edited source before it stores anything. An edit that does not compile is refused with RUST_COMPILE_FAILED, and the function keeps serving its last source. An edit that compiles serves from the next call. Then repeat steps 3 to 5.
7. The version loop
A version holds JavaScript functions only. A Rust function is never in a version: substrio push finds functions/hello.rs, leaves it out of the version, and deploys it to the app with function.deploy, so it is live at once and not in the sandbox first. A [functions.hello] table in substrio.toml works for a .rs file as it does for a .js file, and a name with both files is refused.
A Rust function deployed this way is a direct change to the app. substrio versions lists it under direct_changes. When you promote a version that does not contain it, the promote is refused with 409 DIRECT_CHANGES, because the promote would delete the function. Promote and leave the Rust function as it is with:
substrio promote --keep-direct
--replace-direct lets the version replace or delete the Rust function instead. Test a Rust function with steps 1 to 6 on the app, as they are written above.
8. A queue consumer
A queue is declared by a version, and a version holds JavaScript functions only. A Rust function consumes a queue after a version names the queue and its consumer, and after function.deploy replaces that consumer with Rust source under the same name. The consumer uses Batch::of(req) and returns an Outcome. Promote later versions with --keep-direct so the Rust consumer stays in place. queues.md describes the queue, and wasm-functions.md has the consumer example.
Related pages
- index.md is the function runtime.
- wasm-functions.md is the reference for the request, the response, the entries, the database, and the limits.
- wasm-records.md, wasm-ledger.md, wasm-services.md, and wasm-platform.md cover the rest of the crate.