WASM Plugins
The WASM tier runs plugins as WebAssembly modules inside the wasmtime runtime. It provides near-native performance with strong memory isolation and deterministic resource limits via fuel budgets. This is a Pro feature.
How It Works
1. You write your plugin in Rust (or Go/C) and compile it to a .wasm module targeting wasm32-wasi.
2. bext loads the module via wasmtime at server start.
3. Each lifecycle call consumes "fuel" -- a wasmtime abstraction that limits computation. When fuel runs out, the call is terminated.
4. Host functions under the "bext" import module provide storage, HTTP, metrics, and queue access.
Fuel Budgets
Each lifecycle hook has a default fuel budget. One unit of fuel roughly corresponds to one WASM instruction.
| Hook | Default Fuel | Approximate Wall Time |
|---|---|---|
on_server_start |
500,000,000 | ~500 ms |
on_request_complete |
100,000,000 | ~100 ms |
on_cache_write |
50,000,000 | ~50 ms |
on_cache_invalidate |
50,000,000 | ~50 ms |
on_reload |
200,000,000 | ~200 ms |
cleanup |
100,000,000 | ~100 ms |
Override budgets in bext.config.toml:
[plugins.my-wasm-plugin]
sandbox = "wasm"
path = "./plugins/my-plugin.wasm"
[plugins.my-wasm-plugin.fuel]
on_server_start = 1_000_000_000
on_request_complete = 200_000_000
Host Function Imports
WASM plugins access host capabilities through functions imported from the "bext" module. All complex types are passed as JSON-encoded strings via (ptr, len) pairs.
Available Host Functions
| Function | Signature | Description |
|---|---|---|
bext.log |
(level_ptr, level_len, msg_ptr, msg_len) |
Structured logging |
bext.storage_get |
(key_ptr, key_len) -> (ptr, len) |
Read from scoped KV store |
bext.storage_set |
(key_ptr, key_len, val_ptr, val_len) -> i32 |
Write to KV store |
bext.storage_delete |
(key_ptr, key_len) -> i32 |
Delete KV entry |
bext.http_fetch |
(req_ptr, req_len) -> (ptr, len) |
HTTP client (rate-limited) |
bext.get_config |
(key_ptr, key_len) -> (ptr, len) |
Read plugin config value |
bext.emit_metric |
(name_ptr, name_len, value: f64, tags_ptr, tags_len) |
Emit metric |
The http_fetch request and response use this JSON format:
// Request
{ "url": "https://api.example.com/data", "method": "POST",
"headers": [["Content-Type", "application/json"]], "body": "{}" }
// Response
{ "status": 200,
"headers": [["content-type", "application/json"]],
"body": "{\"result\": \"ok\"}" }
Sandbox Permissions
[plugins.my-wasm-plugin.permissions]
allowed_urls = ["https://api.example.com/*"]
storage_quota_kb = 1024
max_fetch_per_minute = 30
kv_namespaces = ["analytics"]
queue_names = ["events"]
- allowed_urls — glob patterns for outbound HTTP. Empty = no network access.
- storage_quota_kb — maximum KV storage per plugin (default: 1024 KB).
- max_fetch_per_minute — token-bucket rate limit for HTTP fetches (default: 30).
- kv_namespaces — shared KV namespaces the plugin can access. Empty = plugin-scoped only.
- queue_names — message queues the plugin can publish to or consume from.
Building a Rust WASM Plugin
1. Create the project
cargo new --lib my-analytics-plugin
cd my-analytics-plugin
2. Set up Cargo.toml
[package]
name = "my-analytics-plugin"
version = "0.1.0"
edition = "2021"
[lib]
crate-type = ["cdylib"]
[dependencies]
bext-plugin-api = "0.1"
serde = { version = "1", features = ["derive"] }
serde_json = "1"
3. Implement the LifecyclePlugin trait
use bext_plugin_api::lifecycle::LifecyclePlugin;
use serde::Deserialize;
#[derive(Deserialize)]
struct Config {
endpoint: String,
batch_size: usize,
}
struct AnalyticsPlugin {
config: Option,
buffer: Vec,
}
impl LifecyclePlugin for AnalyticsPlugin {
fn name(&self) -> &str {
"my-analytics"
}
fn priority(&self) -> u32 {
1000
}
fn on_server_start(&self, config_json: &str) -> Result<(), String> {
let config: Config = serde_json::from_str(config_json)
.map_err(|e| format!("parse config: {e}"))?;
// Store config for later use
bext::log("info", &format!("Analytics endpoint: {}", config.endpoint));
Ok(())
}
fn on_request_complete(&self, event_json: &str) -> Result<(), String> {
// Buffer events, flush when batch is full
bext::storage_set("pending", event_json)?;
bext::emit_metric("analytics_events", 1.0, &[("source", "wasm")]);
Ok(())
}
fn cleanup(&self) -> Result<(), String> {
bext::log("info", "Flushing remaining analytics events");
Ok(())
}
}
4. Compile to WASM
cargo build --target wasm32-wasi --release
cp target/wasm32-wasi/release/my_analytics_plugin.wasm ./plugin.wasm
5. Configure in bext
[plugins.my-analytics]
sandbox = "wasm"
path = "./plugins/my-analytics/plugin.wasm"
[plugins.my-analytics.config]
endpoint = "https://analytics.example.com/ingest"
batch_size = 100
[plugins.my-analytics.permissions]
allowed_urls = ["https://analytics.example.com/*"]
max_fetch_per_minute = 60
Debugging WASM Plugins
Enable WASI logging to see stdout/stderr from your WASM module:
BEXT_PLUGIN_LOG=debug bext dev
wasmtime traps (out-of-fuel, memory violations, unreachable instructions) are logged with a full stack trace when debug logging is enabled.
To profile fuel consumption:
bext plugin stats my-analytics
This reports per-hook fuel usage, call counts, and average execution time.
Go and C Plugins
Any language that compiles to wasm32-wasi works. For Go, use TinyGo:
tinygo build -o plugin.wasm -target=wasi ./main.go
For C, use wasi-sdk:
$WASI_SDK_PATH/bin/clang --sysroot=$WASI_SDK_PATH/share/wasi-sysroot \
-o plugin.wasm plugin.c
The host function ABI is the same regardless of source language -- JSON strings passed via (ptr, len) pairs.