WASM Plugins
Build high-performance plugins in Rust, Go, C, or Zig — compiled to WebAssembly with strong typing via WIT.
WASM plugins run at near-native speed with strong sandboxing. Use any language that compiles to WebAssembly.
Why WASM?
| Script Plugins (JS/Lua/Rhai) | WASM Plugins | |
|---|---|---|
| Performance | Good | Near-native |
| Language | JS / Lua / Rhai only | Rust, Go, C, Zig, etc. |
| Typing | Dynamic | Strong (WIT interface) |
| Concurrency | Per-request (no state) | Instance pool (stateful) |
| Best for | Quick integrations, hooks | Heavy computation, data processing |
Quick Start (Rust)
1. Create the project
cargo new --lib my-plugin
cd my-plugin2. Add dependencies
[lib]
crate-type = ["cdylib"]
[dependencies]
wit-bindgen = "0.36"3. Write the plugin
Download the plugin.wit interface file, then:
wit_bindgen::generate!({
path: "../plugin-wit/plugin.wit",
world: "plugin-world",
});
use exports::raisfast::plugin_wit::plugin_hooks::*;
struct Plugin;
impl Guest for Plugin {
fn on_post_creating(input: PostInput) -> Option<PostInput> {
let mut post = input;
if post.slug.is_none() {
post.slug = Some(post.title.to_lowercase().replace(' ', "-"));
}
Some(post)
}
fn on_post_created(output: PostOutput) {
host_api::log("info", &format!("published: {}", output.slug));
let key = format!("cache/posts/{}.json", output.slug);
host_api::vfs_write(&key, "{}");
}
fn on_cron_tick(payload: Option<String>) {
if let Some(p) = payload {
host_api::log("info", &format!("cron fired: {p}"));
}
}
}
export_plugin_hooks!(Plugin);4. Build
cargo build --target wasm32-unknown-unknown --release
cp target/wasm32-unknown-unknown/release/my_plugin.wasm plugin.wasm5. Manifest
[plugin]
id = "com.example.my-plugin"
name = "My WASM Plugin"
version = "0.1.0"
runtime = "wasm"
entry = "plugin.wasm"
[hooks.on-post-creating]
priority = 10
[hooks.on-post-created]
priority = 10
[[cron]]
label = "Scheduled Task"
job_type = "my_task"
cron_expr = "0 */6 * * *"6. Deploy
extensions/plugins/my-plugin/
├── manifest.toml
└── plugin.wasmRestart the server or let hot reload pick it up.
WIT Interface
WASM plugins implement the plugin-hooks interface and import host-api.
Host API (imports)
Functions your plugin can call:
interface host-api {
log: func(level: string, msg: string);
get-config: func(key: string) -> option<string>;
http-get: func(url: string) -> option<string>;
http-post: func(url: string, body: string) -> option<string>;
get-data: func(key: string) -> option<string>;
set-data: func(key: string, value: string) -> bool;
get-post: func(slug: string) -> option<string>;
db-query: func(sql: string, params: option<string>) -> string;
db-execute: func(sql: string, params: option<string>) -> string;
db-begin: func() -> string;
db-commit: func() -> string;
db-rollback: func() -> string;
vfs-read: func(path: string) -> option<string>;
vfs-write: func(path: string, content: string) -> bool;
vfs-delete: func(path: string) -> bool;
vfs-exists: func(path: string) -> bool;
vfs-list: func(path: string) -> option<string>;
vfs-stat: func(path: string) -> option<string>;
emit-event: func(event-type: string, data: string) -> string;
}Plugin Hooks (exports)
Functions your plugin can export:
interface plugin-hooks {
// Filters — return Some to modify, None to skip
on-post-creating: func(input: post-input) -> option<post-input>;
on-post-updating: func(input: post-input) -> option<post-input>;
on-comment-creating: func(input: comment-input) -> option<comment-input>;
on-content-creating: func(input: content-event) -> option<content-event>;
on-content-updating: func(input: content-event) -> option<content-event>;
render-markdown: func(input: string) -> option<string>;
filter-html: func(input: string) -> option<string>;
// Actions — return value ignored
on-post-created: func(output: post-output);
on-post-updated: func(output: post-output);
on-post-deleted: func(id: string);
on-comment-created: func(input: comment-input);
on-content-created: func(input: content-event);
on-content-updated: func(input: content-event);
on-content-deleted: func(content-type: string, id: string);
on-login: func(user-id: string);
on-cron-tick: func(payload: option<string>);
}WIT Types
record post-input {
title: string,
content: string,
slug: option<string>,
excerpt: option<string>,
category-id: option<string>,
tag-ids: option<list<string>>,
status: option<string>,
cover-image: option<string>,
}
record post-output {
id: string,
title: string,
slug: string,
content: string,
excerpt: option<string>,
status: string,
created-by: string,
updated-by: option<string>,
category-id: option<string>,
view-count: s64,
created-at: string,
updated-at: string,
published-at: option<string>,
}
record comment-input {
content: string,
nickname: option<string>,
email: option<string>,
parent-id: option<string>,
}
record content-event {
content-type: string,
data: string,
id: option<string>,
}Concurrency Model
Unlike JS/Lua (per-request, stateless), WASM uses an instance pool:
- N pre-compiled instances (configurable via
PLUGIN_WASM_POOL_SIZE, default 4) - Round-robin dispatch for concurrent requests
- Instances are reused — you can hold state between calls
- Perfect isolation between instances
Current Limitations
The WIT interface provides only raw SQL (db-query, db-execute). The following functions available in JS/Lua/Rhai are not yet exposed to WASM:
- High-level DB API:
dbInsert,dbFetchOne,dbFetchAll,dbUpdate,dbDelete,dbCount,dbIncrement,dbSum,dbGroupBy newId(UUID v7 generation)dbPh(SQL placeholder)
Use raw SQL with db-query and db-execute as a workaround.
Using Other Languages
Any language that targets wasm32-unknown-unknown and supports the Component Model works:
| Language | Bindgen Tool |
|---|---|
| Rust | wit-bindgen |
| Go | wit-bindgen-go |
| C | wit-bindgen-c |
| Zig | Community tooling |
The WIT interface file is the single contract — as long as your language can implement it, you're good.
